MATLAB面向对象编程模式上

写在之前

因为简书字数限制,完整版地址:https://www.zybuluo.com/hainingwyx/note/609905
如有错误,烦请指正!

第1章 面向过程和面向对象程序设计

1.1 什么是面向过程的编程

定义:一种以过程为核心的编程算法,把问题的过程按照步骤分解出来,然后用函数形式加以实现。
面向过程编程方法的优点是简单快捷,缺点是面对复杂的程序难以修改和维护。

1.2 什么是面向对象的编程

面向对象编程(Object Oriented Programming,OOP)把任务分解成一个个相互独立的对象,通过各对象的组合和通信来模拟实际问题。

1.2.1 什么是对象(Object)

真实世界中具体的东西。
特点:具有各种属性;具有相关的行为。

1.2.2 什么是类(Class)

对各个具体、相似对象的共性的抽象。

1.2.3 什么是统一建模语言(UML)

Unified Modeling Language:对程序的一种图形表达方式
Matlab要求每个类的定义保存为一个同名的文件。
在matlab中建立实体对象的方式是:调用类的构造函数(Constructor)。构造函数和类同名;构造函数的返回值是构造出来的新的对象。

1.3 面向过程编程有哪些局限性

不容易维护和扩张。大多数情况下,如果已有了面向过程的程序,可以用面向对象的思想去包装这些已有的程序,并且在此基础上继续维护和扩张已有的程序。

1.4 面向对象编程有哪些优点

把大问题分解成小的对象,通过组合和信息传递完成任务,通过继承达到代码的复用,修改或添加模块不会影响到其他模块

第2章MATLAB面向对象程序入门

2.1如何定义一个类

classdef Point2D<handle
    properties 
    end
    methods
    end
end

类定义中包含一个属性block和一个方法block。
constructor构造方法:负责产生并且返回类的对象,通常还可以用来初始化对象的属性。

2.2 如何创建一个对象

创建对象的方式是直接调用类的constructor

2.3 类的属性(Property)

2.3.1 如何访问对象的属性

面向对象编程中,使用Dot运算符来访问对象的属性。

2.3.2 什么是属性的默认值(DefaultValue)

在MATLAB类的property Block定义中,可以为属性直接赋一个值,此时为默认值。
如果默认值的赋值使用表达式,该表达式仅在类定义被MATLAB装载时执行一次,所以表达式计算的结果最好是固定。
除了在property block中给属性赋默认值,还可以在constructor对属性变量做初始化。

2.3.3 常量(Constant)属性

定义:在对象生存周期中值保持不变的属性,如果对该属性进行修改都将报错。
定义constant property需要用constant关键词。如

classdef A<handle
    properties(Constant)
        R=pi/180;
    end
end

如果不显示给定常量属性一个特定的值,则默认为empty double。另外constant property不用创建对象就可以直接使用类中的常量

A. R  % A是类名而不是对象名

2.3.4 非独立(Dependent)属性

定义:其值依赖于其他的属性,一旦其他的属性改变,该属性也做相应的变化,在概念上可以理解为数学中的因变量。为了不需要每次都在自变量变化之后更新因变量,可以把该值设为Dependent(依赖)属性。
Dependent属性特点:对象内部没有给属性分配物理的存储空间,每次该属性被访问时,其值将被动态地计算出来。计算该属性的方法由get提供。

classdef Point2D<handle  
    properties
        x
        y
    end
    properties(Dependent)
        r
    end
    methods
        function obj=Point2D(x0,y0)
            obj.x=x0;
            obj.y=y0;
        end
        function r=get.r(obj)            % Dependent属性要放在get方法中
            r=sqrt(obj.x^2+obj.y^2);
            disp('get.r called');
        end            
    end    
end

验证程序:

p1=Point2D(1.0,2.0);
p1.r              % 2.2361
p1.y=1;
p1.r              % 1.4142

设置为Dependent还有一个好处:支持dot和向量化操作。如果r是一个矩阵或者矢量,在类的外部,可以直接进行适量操作(obj.r(1:2));如果r是一个结构体,可以直接使用dot继续访问r内部的其他fields(obj.r.otherfields)
另一个例子:

classdef View<handle
    properties
        hFig
        hEdit
    end
    properties(Dependent)
        text
    end    
    methods
        function obj=View()
            obj.hFig=figure();
            obj.hEdit=uicontrol('style','edit','parent',obj.hFig);
        end
        function str=get.text(obj)
            str=get(obj.hEdit,'String');
        end
    end    
end

什么是matlab的解释器?
回答:MATLAB解释器将MATLAB命令行、脚本、函数中的MATLAB的代码翻译成内部指令,并且执行。MATLAB作为一种解释型语言,从写代码到执行代码的转换是立即完成的,并且源代码总是存在,一旦出现错误,MATLAB解释器就能很容易找出错误的位置。

2.3.5 什么是隐藏(Hidden)属性

隐藏的效果是在命令行中查看对象的信息时,该属性不会被显示出来。

classdef A<handle
    properties(Hidden)
        var
    end
    methods(Hidden)
        function internalFunc(obj)
            disp(‘I am a hidden function’);
        end
    end
end

用户如果知道属性的名字仍然可以正常地访问该属性。
Hidden关键词用处是隐藏类的内部的细节。

2.4 类的方法(Method)

2.4.1 如何定义类的方法

methods
    function [returnValue]=functionName(arguments)
        %…….
    end
end

如果成员方法只有几行可以放在类定义中;否则可以在类中仅给出方法的声明,把实现放到一个独立的文件中去。

2.4.2 如何调用类的方法

  1. 使用OOP的点(Dot)语法调用成员方法obj.normalize()
  2. 使用传统的函数语法调用成员normalize(obj)

2.4.3 点调用和函数式调用类方法的区别

  1. p1.normalize符合面向对象风格,程序的可读性高,一目了然。
  2. 使用Dot语法清楚地告诉MATLAB要调用的是成员方法还是函数。确定用户到底是在调用函数还是类方法,是dispatcher的工作,唯有在执行时,dispatcher才会参与工作。
  3. 使用Dot语法,MATLAB的语法检查器会及时帮助用户检查语法错误。

2.4.4 方法的签名

签名:对象连同方法的名称构成了该方法在matlab中独一无二的签名。
函数签名=函数名+所属类
每次调用对象的方法时,MATLAB的dispatcher都会动态地判断该方法的签名。虽然从表面看两个属于不同类的对象可能调用了方法名称相同的方法,但是MATLAB还是可以通过判断该函数所属类来找到匹配对象的。
脚本中的clear classes命令是必须的吗?
回答:是的,作为一个良好的编程习惯,在每个程序开头使用clear清除残存的变量和旧的定义是必要的。特别是在类的定义被修改之后。我们要清楚内存中旧的定义,这样才能使新的修改生效。

2.4.5 类、对象、属性、方法之间的关系

类是一种抽象的定义,包括property和method,占用内存。对象是一个物理实体,根据类模板创作出来的,也有物理内存,构成对象本身的只有数据。方法是类具有的一种操作,被所有类共享。

2.4.6 用disp方法定制对象的显示

在MATLAB面向对象编程中,可以通过定义类的disp方法(也叫覆盖override)来定制对象在命令行上的输出内容,包含定制disp方法的类的对象,在命令行被用户查询时,会优先调用类的disp方法。

function  disp(obj)
    s=sprintf('%-17s(%s:%s)\n',obj.name,obj.exchange,obj.symbol);
    s=[s,sprintf('--------------------------------------------\n')];
    s=[s,sprintf('Last Trade:       %6.2f',obj.last_price)];
    s=[s,sprintf('  (%s %s)\n',obj.last_time,obj.last_date)];
    disp(s);
end

2.5 类的构造函数(Constructor)

2.5.1 Constructor

定义:是一种特殊的成员方法。和类的名称相同,用来创造类的实例。类定义中只能有一个constructor。constructor只能有一个返回值,且必须是新创建的对象。

2.5.2 property赋值

MATLAB中声明一个对象时,工作顺序是:先装载类的定义,然后调用constructor。

2.5.3 如何让Constructor接受不同数目的参数

MATLAB是弱解释性语言,不能通过参数的数目的不同来决定调用哪个参数,类似的功能只能放到函数体中,通过判断参数的个数(nargin)来实现,根据nargin的不同,选择不同的代码。

function obj=Point2D(x0,y0)
        if nargin==0    %没有提供参数,default constructor
            %.....
        elseif nargin==2
            %......
        else
            %......
        end

2.5.4 Default Constructor

定义:不带任何参数的构造函数

2.5.5 一定要定义Constructor吗

如果用户没有提供任何形式的constructor,MATLAB会在内部提供一个default Constructor。看似一个空函数,但后台还是有工作的,如给对象、属性分配空间等。该自动提供的constructor不解释任何参数,如果尝试提供任何参数,将会报错。

2.6 类的继承

2.6.1 什么是继承

继承关系也叫做泛化关系,被继承的类叫做父类或者基类,继承的类叫做子类或者派生类。在MATLAB中可以用isa查询一个对象是否属于一个特定的类。如:isa (p2,’Point2D’);
UML中继承用空心三角箭头表示

2.6.2 为什么子类Constructor需要先调用父类Constructor

子类先继承了父类的属性和方法,然后在父类的基础上增加自己的属性和方法。代码中的@表示调用父类的constructor,返回一个对象。

obj=obj@Super();

2.6.3 在子类方法中如何调用父类同名方法

句柄:句柄是一种抽象的思想:隐藏了内核实现的细节,同时为调用提供了方便,保证了内核的安全。
参考文章:http://blog.chinaunix.net/uid-26285146-id-3262293.html
子类和父类成员方法可以有相同的名字,并且在子类方法内部可以调用父类的同名方法,在子类中,除同名方法外的其他地方不能调用父类的同名方法。
调用语法:superMethod@superClass(this,otherArguments)

  • superMethod:父类同名函数
  • uperClass:父类类名
  • this:子类对象的Handle传递给父类,obj
  • otherArguments:向父类同名函数传递其他参数

2.6.4 多态

概念:建立在继承的基础上。同名的方法被不同的对象调用,能产生不同的行为(形态)。

obj1=Print2D();
obj1.print();
obj2=Print2D();
obj2.print();

2.7 类之间的基本关系:继承、组合和聚集

2.7.1 判断能否继承

使用继承可以提高程序的复用性,但是盲目继承会造成逻辑混乱和程序适用性降低。所如果A和B不相关,不能为了B的功能更多些继承A的功能和属性。

2.7.2 企鹅和鸟之间是不是继承关系

共有继承可以用is a“是一个”等价

2.7.3 类组合

如果在逻辑上A是B的一部分,则不要为了让A得到B的功能去继承B,而是要用A和其他东西组合出B。如眼鼻口耳、头的组合关系。组合通过Head对象的constructor来保证。组合关系要求Head对象一定在内部拥有Nose、Eye、Mouth、Ear。
UML图上,实心菱形箭头表示组合关系。

2.7.4 什么是组合聚集关系

组合:整体和部分,
聚集:松散的整体和部分,自行车由架子、轮子、坐垫组成。反映在程序上就是,Wheel和Seat对象可以独立创建。
UML图上,空心菱形箭头表示聚集。

2.8 Handle类的set和get方法

2.8.1 set方法

set方法给对象属性的赋值提供了一个中间层,可以检查赋值是否符合要求或者检查赋值的类型。还可以在set方法中做一些其他工作,比如做一个LOG记录每次属性被赋的值。

classdef A<handle
    properties
        a
    end
    methods
        function set.a(obj,val)
            if val>=0;    %检查赋值要求
                obj.a=val;
            else
                error('a must be positive');
            end
        end        
    end    
end

main:

obj=A();
obj.a=-10    %试图给a赋负数

load一个对象时,属性set方法会被MATLAB调用,如果这时对象的某些属性值仍是默认值,这些属性会经过set方法,被验证有效性。可以认为是数据有效性的最后一层屏障。

2.8.2 get方法

get方法提供了对成员属性查询操作的一个中间层。可以使程序变得向后兼容。比如一个大型程序的升级,需要把date的名字改成timeStamp。

classdef Record <handle
    properties(Dependent,Hidden)
        date%设置成Dependent不占用内存,Hidden属性使得不会被命令行显示出这个旧的属性
    end
    properties
        timestamp
    end
    methods
        function set.date(obj,val)
            obj.timeStamp=val;
        end
        function val=get.date(obj)
            val=obj.timeStamp
        end
    end
end

set和get方法还是有时间成本的。

2.9 属性和方法的访问权限

2.9.1 public,protected,private权限

  • Access=private:表示只有该类的成员方法可以访问该数据,而子类和其他外部函数无法访问该成员变量。
  • Access=protected:表示只有该类的成员方法还有该类的子类而已访问该数据。
  • public:在类的定义中、类的成员方法、子类的成员是方法都可以访问这个成员变量,类之外的函数或者脚本也可以访问这个成员变量。
classdef SomeClass < handle
    properties(Access = private)     %私有属性
        prop_private
    end
    properties(Access=protected)      %保护属性
        prop_protected
    end
    properties(Access=public)      %默认是共有的
        prop_public
    end
    
    methods(Access=privated)%同上
        function result=someFunction(obj)
            %...
        end
    end    
end

UML图中,+表示public,#表示protected,-表示private。
属性的访问权限可以被细分为赋值SetAccess和查询GetAccess的权限。如:

properties(SetAccess=private, GetAccess=public)
    var
end

表明该属性可以被外界程序查询值,但不能被外部程序赋值,赋值只能在类的内部进行。

2.9.2 确定类的属性和方法的权限

classdef BankAccount < handle
    properties(SetAccess=private)
        balance
        accountNumber
    end
    methods
        function obj=BankAccount(balance, num)
            obj.balance=balance
            obj.accountNumber=num
        end
        function deposit(obj,val)
            obj.balance=obj.balance+val
        end
        function withdraw(obj,val)
            obj.balance=obj.valance-val
        end
    end    
end

2.9.3 MATLAB对属性访问的控制与C++和Java有什么不同

public属性,在C++和Java中,如果把一个成员属性定义成public,意味着该属性可以被外部直接访问和赋值,这相当于该属性直接暴露给外部。matlab在外部直接访问和public属性之间可能还存在一个set和get的中间层,即使是public属性,Matlab面向对象语言提供了检查措施。
private属性,在C++和Java中,如果把一个成员定义成private,意味着不可以被外部直接访问和赋值,但是习惯上会提供一个public的set和get方法。两者有差别,前者只是普通方法。C++和Java是现实调用set方法,matlab是隐式调用set方法。

2.10 clear classes到底清除了什么

matlab 2014b引入了新的自动更新类定义的功能。
类被首次使用,Matlab会将类定义一次性装载进内存中之后再声明类的对象时,不需要重新读入类定义。面向对象编程时,如果定义了一个类,声明了一个对象,然后改变了该类的定义,再尝试声明一个对象时,会出现Warning。只要有旧的instance存在,新的类定义就无法生效。通常可以

  1. clear obj
    如果工作空间中还有其他重要变量存在,用户不希望全部清除他们,可以有选择地清除对象。如:工作空间中有A类的对象obj1和obj2,我们修改A类的定义,只要执行,就可以在下次声明A的对象时,使用新的定义。
clear obj1 obj2;
  1. clear classes
    清除工作空间所有变量、所有之前被装载的类定义、类中的constant属性

第3章 MATLAB的句柄类和实体值类

3.1 引子:参数是如何传递到函数空间中去的

如果参数的值在函数内部没有改变,函数的工作空间只是复制了参数的必要数据,而实际数据仍然在函数工作空间之外。这些必要数据提供了访问实际数据的一种渠道(指针)。

function result = myAdd(a,b)
    result=a+b;
end

如果函数体内修改了参数的值,在参数的值改变的前一刻,Matlab会在函数工作空间中复制一个局部拷贝,以保证所有对a矩阵的修改都是局部的,即函数体内对a的修改不会影响到Main工作空间中的a矩阵。这种技术也叫Lazy Copy。不到万不得已不构造局部拷贝。

function result=myAdd(a,b)
    a=round(a);
    result=a+b;
end

3.2 MATLAB的ValueClass和HandleClass

3.2.1 什么是ValueClass和HandleClass

两者的区别是:用户定义的类是否继承了Matlab内部提供的一个Handle基类。
ValueClass:实例化后是一个类的对象,它复制时是拷贝,得到不同的独立对象;
HandleClass:实例化后是类的句柄,复制时是引用,都指向同一个类。

clear all;clc;
mValue=ImageValue(10000);
mHandle=ImageHandle(10000);

Matlab依次在内存中对mValue和mHandle各分配了一个对象,每个对象大小都为763M。用whos检查对象大小,mValue为763M,mHandle为112字节。这是因为Matlab处理实体类对象的方式是:直接在内存中开辟一块区域,用以存放实体类对象;Matlab处理句柄类对象的方式稍微多了一层,在内存中不但有一部分区域用来存放实际有用的数据,还有一个句柄对象,指向这块内存。句柄对象为112字节。

3.2.2 Value类对象和Handle类对象拷贝有什么区别

nValue=mValue;
nHandle=mHandle;

Value类对象在内存中进行了完全拷贝,也叫深拷贝;
Handle类对象只拷贝Handle类对象本身,而没有拷贝句柄对象指向的实际数据。这是一个不完全的拷贝,也称浅拷贝。因为他没有深入到handle所指向的数据对实际数据进行拷贝。特征:如果通过nHandle修改matrix属性,也会影响mHandle的matrix属性,因为这两个Handle对象共享了一份matrix数据。

3.2.3 Value类对象和Handle类对象赋值的区别

不到万不得已matlab不会在内存中构造一个一模一样的拷贝,这个万不得已就是当nValue中的matrix的值做出改变时,当我们哪怕仅仅修改nValue矩阵中的一个元素的值,Matlab在内存中马上就构造出一个实体的拷贝,然后再修改其中元素的值。

clear all;
mValue=MatrixValue(10000);
nValue=mValue;
nValue.matrix(1,1)=10%仅改变一个值

3.2.4 Value类对象和Handle类对象当做函数参数有什么区别

function assignVar(obj,var)
    obj.var=var;
end

aValue.assignVar(20):没有起到作用。
对于Value类对象,参数传递方式是:拷贝必要的信息到函数的工作空间中,当MATLAB发现该函数对传入的参数进行了修改,并且该参数是Value类对象时,MATLAB就构建出了一个局部拷贝,所以对obj.var的修改只是局部的,函数退出时,该拷贝也就消失了。所以如果想把函数内部的修改保存下来。必须把obj当做输出参数返回。assignVar必须改为:

function obj=assignVar(obj,var)
    obj.var=var;
end

对于Handle类对象,参数的传递方式也是构造一个局部拷贝对象,但在函数内部,函数修改的不是针对Handle对象的而是Handle对象所指向的数据。所以成功修改了Handle对象所指向的数据。

function assignVar(obj,var)
    obj.var=var;
end

bHandle.assignVar(20);

3.2.5 什么情况下使用Value类或Handle类

Value类适用于比较简单的数据,如果使用者不在意副本的存在,并且希望每次执行对象的拷贝都可以得到一个独立的副本。如果一个数据在其他地方有副本,并且我们希望修改其中一个副本,其他所有的副本都被修改,可以使用Handle类。
从实用计算的角度来说,如果我们的数据体积比较大,我们希望这些数据在各个方法、函数之间传递迅捷,不需要被局部拷贝,则可以用Handle类来包装数据。
从物理角度看,如果类的对象对应一个独一无二的物理对象,设计成Handle类拷贝的是一个访问他们的通道,设计成Value类则无法解释一个独一无二的个体,在程序上有两个全同的拷贝。
从功能的角度看,Value类没有提供任何内置的方法,Handle类则提供了delete方法等,功能也更加强大。
例如5¥*2=10¥
两张5元换成10元,是一个新的对象,所以设计成Value类。

3.3 类的析构函数(Destructor)

3.3.1 对象的生存周期

定义:从对象产生到释放的过程

3.3.2 析构函数(Destructor)

定义:在对象脱离作用域或者被销毁时,负责收尾工作,比如关闭文件句柄、释放数据所占内存空间等。MATLAB规定:这类方法要命名为delete。无论Value或者Handle都需要用户定义自己的delete函数。

3.3.3 对Object使用clear

  1. Value类:直接把这个对象从工作空间中删除。
  2. Handle类:清除Handle对象本身,Handle对象指向的实际数据是否被清除取决于该数据是否还被其他Handle所指向。

3.3.4 对Object使用delete

对Handle类对象使用delete将释放该Handle所指向的数据。但是Handle对象还是存在的,可以指向其他的内存数据。

3.3.5 delete方法的自动调用

  1. 对Handle类对象重新赋值,例如:mHandle被重新赋值之前所指向的内存数据引用计数是1,重新赋值之后,引用计数为0,delete方法将被调用。
  2. 工作空间使用clear命令,所有工作空间的Handle对象的delete方法都会被调用。
  3. 对象离开了作用域
  4. A和B都是Handle类,B对象隶属于A对象,A对象的销毁将导致B对象的delete函数被调用。

是不是所有局部对象离开作用域之后都会被销毁?
回答:所有用户定义的类的对象和大多数MATLAB内置的类的定义出来的对象,都具有这样的行为,这类对象通常叫做Scoped Object。但有少数MATLAB内置类定义的对象叫做User-Managed Object,离开作用域后,不会自动被销毁。User-Managed,即需要用户指示MATLAB去销毁。例如:timer、analoginput、videoinput等。

3.3.6 出现异常时delete函数如何被调用

面向对象程序异常时,直到MATLAB捕获异常之前,所有try catch所处的函数的堆栈之上的,已经声明的对象,他们的析构函数都会自动调用。

3.3.7 何时需要自己定义一个delete方法

当类的对象占用可一些系统资源,而无法自动释放时,需要自己重载一个delete方法释放这些系统资源。比如文件的打开一般需要删除。对于普通属性,MATLAB在销毁对象时会自动释放这些属性所占用的内存。

  1. Value类没有析构函数
    Value类delete方法只是用户自己定义的,不是matlab承认的合法的析构函数,所以MATLAB不会在value类对象离开作用域是自动调用,所以需要用户显式调用。valObj.delete();
  2. MATLAB自动调用handle类对象的析构函数
    如果用户有额外需要,必须自己在子类中重载delete方法。如果有可能,还是应主动调用delete方法;否则MATLAB会在Handle对象离开其作用域时才会调用clear函数,从而触发delete方法。
  3. Handle类的合法析构函数
  • 方法的名字叫做delete
  • 没有返回值
  • 只接受一个参数,且参数必须是obj,即对象本身
  • 方法不允许是Sealed 或者Static,或者Abstract的,但是delete方法可以是private
    如果delete方法没有满足上述中的任意一点,仍可显式调用该函数,但是不能自动调用。

第4章 事件和响应

4.1 事件(Event)

4.1.1 事件定义

定义:泛指对象内部状态的改变。
在事件发生和出发响应的模型中,通常把改变内部状态的对象叫做发布者,而把监听事件并作出响应的对象叫做观察者。

4.1.2 如何定义事件和监听事件

MATLAB规定:事件的定义要放在event block中。

events
    dataChanged
end
obj.notify(‘dataChanged’);

notify方法:监视其数据变化的对象发布的消息。
addlistener:用来在发布者处登记观察者。
方法addlistener用来构造监听者,登记的相应函数可以是普通函数,也可以是类的成员方法,还可以是类的静态方法。

lh=addlistener(eventObj,’EventName’,@functionName);
lh=addlistener(eventObj,’EventName’,@Obj.methodName);
lh=addlistener(eventObj,’EventName’,@ClassName.MethodName);

第一个参数是发布者对象(Src);第二个参数是事件的数据(eventdata)
matlab使用addlistener方法在发布者和观察者之间建立一个中间层,发布者只接受addlistener方法构造出来的listener对象在其处注册,真正的观察者只需要把自己的响应函数提供给addlistener方法,addlistener方法将把这个响应函数的句柄包装在其构造的对象的内部。这样就实现了用户定义的Handle类与具体的各种观察者之间的解耦合。

4.1.3 为什么需要事件机制

4.2 发布者通知观察者对象,但不传递消息

4.3 发布者通知观察者,并且传递消息

因为event.EventData类是Handle类,所以任何用户定义的事件类本身也是handle类,因此event还可以用来传递大型数据。假设传递一个500500的矩阵作为消息,并且有十个观察者在发布者出注册,notify所有观察者的成本仅是构造一个message对象,传递10次handle。因为500500只存在一个拷贝,且被10个观察者共享。更节省空间的办法是:提供一个内部数据的公共接口给观察者。

4.4 删除listener

因为listener对象本身就是handle对象,所以删除一个listener对象时,只需要调用handle基类提供的delete方法。

第5章 MATLAB类文件的组织结构

5.1 使用其他文件夹中的类的定义

在其他路径上使用类的定义,要用addpath命令,把包含在该类的文件夹加到MATLAB搜索路径中去。

addpath('Z:\folder1\folder2');                    % 添加文件夹
addpath(genpath('c:/matlab/myfiles'));      %添加文件夹以及子文件夹
p1=Point(1,1);

5.2 类的定义和成员方法的定义分开

另一种定义类的方法:在类定义文件中仅仅提供方法的申明,不提供方法的定义,方法的定义放到另一个独立的.m文件中。适合成员方法比较多的情况,一个项目多人开发时,有利于团队代码的版本管理。
MATLAB规定,如果把方法的定义normalize和display放在单独的文件夹中,那么类定义Point.m、normalize.m、display.m必须放在一个以@开头的文件夹中,且该文件夹必须命名为@Point。

哪些方法一定要放在类定义中?

  1. 类的constructor和delete方法的定义
  2. 任何属性的get和set方法的定义
  3. 类的static方法的定义

放在@文件夹中的任何方法都被默认为类的成员方法。如果由于疏忽没有某方法的申明,那么该方法的属性将使用方法的默认值。(非隐藏、public)

如何使用@Point中类的定义?

  1. main程序需要使用PointClass的类定义,只需要放在和该文件夹同一目录下即可。
  2. addpath把@Point文件夹添加到matlab的搜索路径,任何路径都可以使用了。

5.3 定义类的局部函数

局部函数不是类的方法,在类定义外部不可见,不能通过obj.method的方式从外部访问。局部函数仅对类定义内部的方法可见。
局部函数对函数参数要求没有限制,不像类的实例方法那样,参数中一定要包含一个对象。所有在同一文件中定义的类方法都能调用局部函数的方法,但那些没有声明的类方法除外。
在classdef外部定义的类的方法也可以拥有自己的局部函数,调用规则和类的局部函数相似。

5.4 使用Package文件夹管理类

5.4.1 Package中的类是如何组织的

如果程序结构再复杂一点,可以把各个类进一步组成package。MATLAB规定package文件目录必须以+开头,package中还可以包括各个类的文件夹,各类之间还可以有继承关系。

5.4.2 如何使用Package中的某个类

如果一个类的定义放在package中,使用该类时要在类名前加上Package的名称。

p2=MyPointPackage.Point3D(1,1,1);

5.4.3 如何导入Package中的所有类

如果在程序的开头就用import命令导入整个Package。这样调用package的时候就不需要使用package的名称了。

import MyPointPackage.*;

5.5 函数和类方法重名到底调用谁

两个function同名,但是定义不同,一个是普通函数,一个是类AClass的成员方法,这种情况下,调用哪个function将取决于用户程序的调用方法。

5.6 Package中的函数和当前路径上的同名函数谁有优先级

MATLAB的package中可以放置普通的函数,如果两个普通的函数,有相同的函数名,一个在当前路径下,一个在package中,且它们的签名是一样的,在main程序中,该函数被调用时,则MATLAB将调用路径上优先级最高的函数。
默认情况下,最直接路径中的函数具有最高优先级。如果用户想要调用的是package中的foo函数,可以先导入整个package。

第6章MATLAB对象的保存和载入

6.1 save和load命令

6.1.1 save和load object

save filename obj:把对象obj的数据保存到名为filename的mat文件夹中
load matfilename obj:把mat文件中的对象obj装载到工作空间中

6.1.2 MAT文件中保存了object中的哪些内容

1、obj所属的类的名称和package的名称
2、obj所属的类的属性的默认值
3、obj中普通属性的值
save时,会把save对象的属性的值和默认值相比较,如果一样,则只保存属性的默认值。一方面节省mat文件的空间,一方面又保持了兼容性。因为如果save时类的某属性值的默认值在load时发生了变化,为了保证装载对象能恢复到其初始的状态,而不是新的状态,load会沿用旧的默认值。

mat文件没有保存:

  1. 对象的transient、constant、Dependent类型的属性
  2. 类的完整定义

保存handle类,如果已经使用delete方法,该对象指向的数据将被释放,但是handle对象仍然存在,但是是一个无效的handle对象。
mat文件不能保存类的定义,所以load对象时必须能找到该对象的类的定义。所以类的定义必须在MATLAB的搜索路径上,且类的定义必须和save时要保持”一致“,如果不一致程序需要付出一定的代价。

6.1.3 类的定义在save之后发生了变化

  1. 属性名称变了
    新变化的属性为空,继续装载下一个属性
  2. 添加了新的属性
    对象被正常装载,并且多了一个属性,且值取新定义中的默认值
  3. 属性被删除了
    对象被正常装载,删除属性不出现在对象中。
  4. 属性的默认值变了
    沿用旧的默认值

6.2 saveobj和loadobj方法

6.2.1 定义saveobj方法

当用户需要扩展或者定制save的行为时,可以在类的定义中重新定义saveobj成员方法,一旦提供了该类的saveobj方法,那么对该对象调用save命令时:save filename obj,MATLAB就会调用该类自己的saveobj方法保存对象。
saveobj返回值是一个struct,saveobj方法把一个object转换成一个struct。struct中field的名称最好和对象属性名称保持一致。

classdef MyClass
    properties
        x=1;
    end
    
    methods
        function s=saveobj(obj)
            s.x=obj.x;%s is a struct
        end
    end
    methods(Static)
        function obj=loadobj(obj)
            if isstruct(obj)
                newobj=MyClass(obj.x);
            end
            obj=newobj;
        end
    end
end
%test:
obj1=MyClass();
save obj.mat obj1
clear all
load obj.mat obj1

6.2.2 如何定义loadobj方法

为了保证MAT文件中的数据能够被正确初始化新的对象,用户还需要提供一个配套的loadobj方法。
loadobj成员方法的参数是一个struct,返回值必须是一个对象。
loadobj必须是一个静态方法,因为在调用loadobj时,类的对象还没有被建立起来,所以只能是静态的。loadobj必须返回一个新构成的该类的对象。
saveobj和loadobj应用:可以用他们保存中间计算的结果。比如自洽或者最优化计算,如果计算量大、时间长,只给出一次初始值,然后等很长时间希望其收敛,显然不是最佳的办法,如果计算到一半程序出错,还要重头再来更不方便。这种情况下应该每个一段时间对结果进行一次saveobj保存,下次重新开始时,再使用loadobj利用上次的保存结果作为新的初始值,根据收敛的情况,适当调整自洽或者优化参数让计算继续。
再比如,我们要遍历大量的数据,单次计算的时间不长,但是计算的次数很多,总体计算时间仍很长。这种情况下,我们也应该考虑使用saveobj把单次计算的结果保存下来,对大量的数据做分段遍历。

6.3 继承情况下的saveobj和loadobj方法

6.3.1 存在继承时的saveobj方法

6.3.2 存在继承时的loadobj方法

给每个loadobj成员方法都构造一个中间层,即在每个类中添加一个方法reload。该中间方法只负责赋值,不负责对象创建。这样父类和子类的loadobj就都可以正常工作了。

6.4 瞬态(Transient)属性

可以直接给属性添加一个特征修饰词,告诉save命令,哪些属性需要保存,哪些属性不需要保存。这些特征修饰词叫做瞬态。
load Mat文件时,若没有对transient属性做特殊处理,该属性load过后,值为空。Transient属性分配内存,但是不会被保存到MAT文件,Dependent属性不分配内存,不会保存到MAT文件中。

6.5 装载时构造(ConstructOnLoad)

如果希望在load过程中,自动给某些属性赋值,可以使用一个类叫ConstructOnLoad的关键词。如果把一个类声明成ConstructOnLoad=ture,那么在包含该类对象的mat文件被加载时,MATLAB会自动调用该类的缺省的构造函数。
只有在ConstructOnLoad和Transient无法解决问题时,才需要考虑重载函数saveobj和loadobj。

第7章 面向对象的GUI编程

7.1 使用GUIDE进行GUI编程

程序由一个主函数和一个FIG文件构成。主函数的GUIDE_GUI_OpeningFcn用来初始化控件的初值,以及balance变量。主函数中有GUIDE自动生成的回调函数的框架,回调函数可以通过handles来访问figure上各个控件对象,做必要的计算和更新。
GUIDE的优点是迅速地构造简单界面,缺点也很明显,当用户界面复杂到一定程度,尤其需要多个界面,并且经常需要修改时,使用主函数+若干子函数,并且用GUIDE界面来布置各个控件对象位置的方法就显得力不从心。

7.2 使用程序的方式(Programmatic)进行GUI编程

  1. 构造初始数据
  2. 产生figure对象和各个控件对象(可以设置layoutmanager来自动设置控件的位置)
  3. 注册响应的回调函数
  4. 设计回调函数

7.3 用面向对象的方式进行GUI编程

MVC(model-view-control)GUI编程

  • model:反应程序的中心逻辑。
  • view:显示user interface。主要职责1是产生Figure对象和控件对象,它们放在什么位置,以及设置默认值,2是把控件和它们的回调函数联合起来:
set(withdrawButton,'callback',@(o,e) withdraw_callback (o,e));
set(depositButton,'callback',@(o,e)deposit_callback (o,e));

MVC把GUI程序分成3部分:

  • model:负责程序的内在逻辑;
  • view:负责构造,展示用户界面;
  • controller:负责处理用户输入。

7.4 Model 类

需要定义一个事件,当值变化时,对model类对象发出通知,
给监听该事件的listener。model对象和View对象之间是被监听和监听的关系。
model对象和View对象之间的关系是监听和被监听的关系。model的余额的改变将触发balanceChanged事件,View响应并更新balance的值。

7.5 View 类

职责

  1. 向用户展示GUI界面。
  2. 在模型中注册listener,监听模型内部状态的变化
  3. 从模型中得到内部状态,并且显示到GUI上。

每一个View对象必须有一个控制器,因为控制器负责处理用户的输入。因为视图类的职责仅仅是展示,不包括响应,所以View类视图还要负责给自己拥有的控件注册回调函数。View类必须拥有model对象的handle,这样才能在updateBalance函数中查询model的内部状态,并且更新界面。
此外控制器对象也将拥有View对象的handle。
回调函数的两种方式

  1. 传统方式:使用set函数,对MATLAB的GUI控件注册回调函数
funcH=@controller.callback_drawbutton;
set(obj.drawButton,'callback',funcH);
  1. 第4章中给用户自定义类中的事件注册回调函数
obj.modelObj.addlistener('balanceChanged',@obj.updateBalance);%注册

7.6 Controller

控制器的职责是:让Model和View解耦,处理来自用户的输入,解释用户和GUI的交互,改变视图类上控件的外观。
注意controller类为了能够调用model对象中的函数withdraw和deposit,必须拥有model对象的handle。

7.7 如何把Model、View和Controller结合起来

Model、View、Controller放到一起
按钮被按下之后发生的事件序列图

withdraw按钮是MATLAB内置的控件对象,被监听的事件是“按钮被按下”,回调函数是withdraw_callback。该函数定义在controller中,在View类中的attachToController方法中被注册。withdraw将造成balanceChanged,有一个listener监听该事件,内部的相应函数是视图类中的updateBalance

7.8 如何设计多视图的GUI以及共享数据

如果只有一个视图,那么使用GUIDE或者面向过程式的设计方式就可以迅速地解决问题。否则就要利用MVC模式或者其他面向对象的设计模式。
多视图的GUI程序一个常见问题就是GUI之间共享数据,也就是模型和对象之间如何共享数据。只要对象互相拥有彼此的handle即可。如:

classdef Model1<handle
    properties
        hModel2
        hModelMain
    end
    methods
        function accessData(obj)
            %直接通过hModel2的handle访问hModel2对象的数据
            temp=hModel2.someProp;
        end
    end    
end

如果各个View、Model、Controller之间数据相互关联,都需要彼此的数据,为避免相互依赖耦合严重,可以设计一个Context类,其作用像一个枢纽,要求其他类的对象在创建时,都在这个上下文类中注册,并且给每个对象一个独一无二的ID,当一个对象需要取得其他类的对象的数据时,就可以通过ID到Context类对象处获取其他类的handle。
因为我们希望在任何路径和函数中都可以随时通过Context对象得到所需要的对象的句柄,所以该对象应该是一个全局对象,而且程序只需要这么一个上下文类,这样就需要限制该类所能产生的对象的数量,所以是singleton。

引入一个枢纽
  1. context类的设计
    核心数据结果是一个MAP容器:其中,register函数要求外部对象(client)提供ID作为Key,并把外部对象的handle作为KeyValue,保存在Map容器中;getData方法通过client提供的ID在MAP容器中查询,并返回注册的handle
  2. 在context对象处注册ID
    先声明两个模型对象,并且每个对象都赋予一个在整个计算中都独一无二的ID,然后通过getInstance静态方法得到该上下文类的对象,在使用register注册。
obj1=ModelDevice('Camera');
obj2=ModelDevice('PowerSource');
contextObj=Context.getInstance();%得到全局上下文对象
contextObj.register('Camera',obj1);%注册Camera
contextObj.register('PowerSource',obj2);%注册PowerSource对象
  1. 从Context对象处查询ID
    获得模型对戏Camera的句柄:只要Context类定义在MATLAB搜索路径上,我们就可以在任何环境中调用getInstance静态方法,context对象就好像一个被封装好的全局对象。提供一个不能处理无效ID的简化的Context类的实现:
function someFunction( )
    contextObj=Context.getInstance();
    hCamera=contextObj.getData('Camera');
end

7.9 如何设计GUI逻辑架构

针对含有Menubar、Toolbar和子视图的GUI结构,一种解决办法是:从基本的MVC模型出发,把各个视图用Composite(组合)关系组织起来,把各个控制器类用parent-child关系组织起来。
View类:MainView可以作为最上层的视图对象容器。子视图对象也可以作为容器。
Model类:只有MainView拥有其他类对象的handle。
Controller类:MainController是最上层的控制器,是其他众Controller的parent。优点在于:底层控制器可以向高层控制器转发其无法处理的请求,响应的处理函数被分层。请求转发:

function button1CallBack(obj)
    obj.parentController.handleRequest(RequestID);
end

RequestID用来标记请求的类型,提供给上层的控制器用以查找响应的相应函数。

7.10 使用GUILayoutToolbox对界面自动布局

7.10.1布局管理器

绝对布局仅适合简单的界面设计,可以把uicontrol的单位的单位设置成normalized,把绝对布局变成相对布局,可以避免重新计算的问题。如果想要按钮在放大Figure时保持原来的大小,须给控件提供一个resize函数,处理鼠标拖拽的相应。
相对布局:

f=figure(‘Menubar’,‘none’,’Toolbar’,’none’,’’Position’,[200,200,100,200]);
uicontrol(‘style’,’pushbutton’,’Units’,’normalized’,’Position’,[0.1 0.4 0.8 0.25],’parent’,f);

7.10.2 纵向布局类VBox

作用:用来对控件进行单列纵向布局。

7.10.3 横向布局类HBox

7.10.4 选项卡布局TabPanel

7.10.5 网格布局类Grid

7.10.6 GUILayout的复合布局

复合布局是指把各个布局管理器对象组合起来,通过多层的parent-Child关系对界面进行更加灵活的设计。

7.10.7 把GUILayoutToolbox和MVC模式结合起来

第8章 类的继承进阶

8.1 继承情况下的Constructor和Destructor

8.1.1 手动调用基类的Constructor

在初始化对象的过程中,如果有参数需要传递给基类Constructor,则需要显式地在子类constructor中调用基类的constructor。

classdef Derived<Base
    properties
        b
    end
    
    methods
        function obj=Derived(a,b)
            obj=obj@Base(a);
            obj.b=b;
        end
    end    
end

8.1.2 自动调用基类的Constructor

如果不需要向Base类的构造函数传递参数,就不需要在Derived的Constructor中显式调用parent的Constructor,MATLAB会隐式地自动帮助用户调用Base类的Constructor。

classdef Base<handle
    methods
        function obj=Base()
            disp('Base CTOR called');
        end
    end
    
end
classdef Derived<Base
    methods
        function obj=Derived()
            %matlab在这里先调用Base的构造函数
            disp('Derived CTOR called');
        end
    end    
end

注意:让MATLAB隐式地调用基类的缺省的Constructor的前提条件是基类必须定义了缺省的Constructor,或者说基类的Constructor必须能够处理零参数的情况,否则MATLAB会报错。

8.1.3 常见错误:没有提供缺省构造函数

MATLAB隐式地自动调用的是缺省的Constructor,如果用户自定义的Constructor没有提供nargin==0的情况处理,所以MATLAB将显示无法找到所需要的构造函数的错误。

8.1.4 在Constructor中调用哪个成员方法

子类可以重新定义基类的方法。如果子类的foo方法override了基类Base的foo方法,如果声明的是基类的对象,在基类的constructor中调用foo方法一定是来自于基类;如果声明一个子类对象,初始化过程中基类的constructor被调用,而基类constructor中又调用了成员方法foo,matlab将调用子类的foo方法。
matlab方法dispatch的规则是:查找方法的signature,而方法的signature由参数列表中的对象和方法名称决定。

8.1.5 析构函数被调用的顺序是什么

在继承结构下,一个类对象的析构函数调用和构造函数调用顺序相反。用户只需要调用子类的delete函数,整个对象在matlab内所占的内存空间就会得到释放,MATLAB会帮用户在子类的delete函数的末尾扩充,调用基类delete函数命令,所以用户不需要在子类的delete函数中显式调用基类的delete函数,且此时忽略delete方法的权限。

8.2 MATLAB的多重继承

8.2.1 多重继承的场合

8.2.2 多重继承概念

包含一个以上的父类的继承。
classdef Derived<Base1&Base2

8.2.3 构造函数被调用的顺序是什么

先调用爷爷,然后大伯二伯。

8.2.4 多重继承如何处理属性重名

如果Base1的属性a是public的,则Base2的属性a设置成private的,如果声明一个Derived类的对象,其属性a的初始值来自Base1.

8.2.5 多重继承中的方法重名

1、基类重名方法中至少有一个方法是private方法
2、用户可以在Derived类中提供另一个同名方法foo,这个方法将覆盖Base1和Base2中的foo方法,从而消除模棱两可的可能性。

8.2.6 什么是钻石型继承

定义:一个基类在继承的层次中多次出现。

8.2.7 如何同时继承Value类和Handle类

实际中,可能同时需要重用Value类和handle类中的代码,可以使用value类的关键词HandleCompatible

classdef(HandleCompatible) BaseV
end

注意:虽然使用了关键词,但是BaseV仍然是Value类。
给Value类注明HandleCompatible,就可以和Handle类一起做父类了,子类为handle类;给Value类注明HandleCompatible,value类一起做父类,子类为value类,但不是HandleCompatible;但是一般不这么做,因为没有意义。

8.3 如何禁止类被继承

使用关键词sealed

classdef (sealed) A
end

另一个办法就是把构造函数声明成private的,由于子类对象的建立必须要访问父类的构造函数,而private的构造函数将禁止子类的访问,所以该错误会出现在运行时,从而达到禁止继承的目的。

第9章 类的成员方法进阶

9.1 Derived类和Base类同名方法之间有哪几种关系

9.1.1 Derived的方法覆盖Base的方法

Base和Derived都定义了foo方法,声明Derived对象,调用该对象的foo方法,实际调用的是Derived的方法,父类的foo方法被覆盖了。

9.1.2 Derived的方法可以扩充Base的同名方法

可以利用foo@Base(obj)调用父类方法,但是只能调用直接的父类的方法。然后可以进行扩充。

9.1.3 Base的方法可以禁止被Derived重写

当基类的作者要确保该方法不被覆盖时,可以在基类方法中使用Sealed关键词。

9.2 什么是静态(Static)方法

也叫作类方法,它为类服务,最明显的特征就是不需要对象就能使用。因为类的Constant Property同样也为类服务,而不属于某个对象,所以静态方法可以访问类的Constant Property。类中的普通方法可以访问静态方法,只需要在方法前面加上类名。
因为静态方法没有把对象当做参数,所以定义类的静态方法既不能访问对象的一般属性,又不能调用类的一般方法。可以访问Constant Property。

9.3 同一个类的各个对象如何共享变量

9.3.1 什么情况下各个对象需要共享变量

各个对象之间共享数据。并且在对象的生存周期中,共享变量的值不变。

9.3.2 如何共享常量属性

如果在对象的生存周期中,共享变量的值不变,那么就可以把该属性声明成Constant,此时该属性被类的所有对象共有,内存中该Constant属性只有一个。

9.3.3 如何共享变量

如果要让类的各个对象共享变量,可以把该对象定义为静态成员方法中的persistent变量。比如可以用一个persistent变量计算对象创造的数量。
为什么MATLAB面向对象语言中不提供static变量?
C++和Java中,让一个类共享数据一般是通过static变量来实现的,但是MATLAB面向对象并没有。原因是:MATLAB长久以来的编程惯例是,在赋值时,变量比方法和类具有更高的优先级。如果添加了这个新功能,将造成向后不兼容,所以MATLAB不支持static变量。

第10章 抽象类

10.1 什么是抽象类(Abstract)和抽象方法

定义:不能被实例化出对象的类。使用关键词Abstract来定义。

classdef
Shape<handle
         methods(Abstract)
                   draw(obj)
         end
end

定义抽象方法,只需要一行声明,不需要具体代码,并且该类的子类包括有一个同名的非抽象的办法。不能定义为Sealed类。

10.2 为什么需要抽象类

子类实现办法不尽相同,所以在抽象类中只声明,不定义,把定义留到具体的子类中完成。

10.3 如何使用抽象类

10.3.1抽象类不能直接用来声明对象

抽象类可以有构造函数,只是不能利用这个构造函数声明出对象来,该构造函数一般被子类构造函数所调用,需要显式调用。构造函数不是抽象的。

抽象方法声明中参数个数不是一个严格的限制。子类只需要实现同名方法即可,参数的数目不一定要和父类中一致。

10.3.2 子类要实现所有抽象方法

抽象类的子类必须实现抽象类中定义的所有抽象办法,否则,该子类仍然是抽象类。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容