Dart语言学习笔记二-对象,接口,类,mixin

此篇内容均来源于《Dart编程语言》--[美]Gilad Bracha 著 戴虬 译

值得再次强调的是:在Dart中,一切皆对象,这甚至包括了最简单的数据如数字或布尔值等。一个对象由(可能为空的)一组字段提供状态,由一族方法提供行为。对象的状态可以是可变或不变的。对象的方法永不为空,因为所有的Dart对象都具备一定的行为。对象从它们的类中获得行为。每个对象都有一个类,我们将之表述为对象是类的一个实例。因为每个对象都有一个决定其行为的类,所以Dart是一门基于类的语言。一个类没有列出父类,那么其父类就是Object。

1.accessor

accessor(存取)是为了方便访问值所提供的特殊方法。
Dart提供的getter方法,是一个不带参数的特殊方法,可以在不提供参数列表的情况下直接调用。getter方法的引入是通过在方法名前添加前缀get。getter方法不需要参数列表, 甚至是空的参数列表。
setter方法名前面要添加前缀set,并只接收一个参数。setter的调用语法与传统的变量赋值一样的。如果一个实例变量是可变的,则一个setter将自动为他定义,所有实例变量的赋值实际上都是对setter的调用。

class Point {
  var rho, theta;
  Point(this.rho, this.theta);
  get x => rho * cos(theta);
  set x(newX) {
    rho = sqrt(newX*newX + y*y);
    theta = acos(newX/rho);
  }
  set y(newY){
    rho = sqrt(x *x + newY*newY);
    theta = asin(newY/rho);
  }
  get y =>rho * sin(theta);
  scale(factor) => new Point(rho * factor, theta * factor);
  operator +(p) => new Point(rho + p.rho, theta + p.theta);
  static distance(p1, p2) {
    var dx = p1.rho - p2.rho;
    var dy = p1.theta - p2.theta;
    return sqrt(dx * dx + dy * dy);
  }
} 

2. 实例变量

当一个类声明一个实例变量时,会确保每个实例都有自己的唯一变量复制。对象的实例变量需要占用内存,这块内存是在对象创建时分配的。重要的是, 此内存在被访问之前, 应该被设置为某些合理的值。在低级语言如c语言中则并不如此,新分配的存储空间的内容可能是不明确的,通常就是内存在重新分配之前的值。这将会导致可靠性,安全性方面的问题。

Dart会将每个新分配的变量(不只是实例变量,还包括局部变量,类变量和顶层变量)初始化为null。在Dart中, 与其他对象一样,null也是一个对象。我们不能把null与其他对象混淆,如0或false。null对象只是在Dart核心库中定义的Null类的唯一实例。

声明实例或静态变量会自动引入一个getter。如果变量是可变的,则一个setter也会被自动定义。事实上,Dart中的字段都不是直接访问的,所有对字段的引用都是对accessor方法的调用。只有对象的accessor才能直接访问他的状态。

3. 类变量

除了实例变量,类也可以定义类变量。一个类只有一份类变量的副本,无论它有多少个实例。即使类没有实例,类变量也存在。

类变量的声明是在变量名前放置单词static。我们可以添加一个类变量来跟踪有多少个实例被创建。

class Box {
  static var numberOfInstance = 0;
  Box() {
    numberOfInstance++;
  }
}

像实例变量,类变量从不直接引用。所有对他们的访问都是通过accessor。在它的声明类中, 类变量可通过名称直接引用。在类的外部,只能通过在变量名前加上类名来访问。

类变量通常也被称为静态变量,当‘静态变量’这个术语也包括了类变量与顶层变量。为了避免混淆,我们将坚持使用‘类变量’这个术语。我们也经常会用“字段”这个术语来通常实例和类变量。

类变量是延迟初始化的, 在getter第一次被调用时类变量才执行初始化,即第一次尝试读取它时。未经初始化,默认值为null。

class Cat{}
class DeadCat extends Cat{}
class LiveCat extends Cat{
  LiveCat(){print("I am alive!");}
}
var schrodingers = new LiveCat() as Cat;
void main() {
  schrodingers = new DeadCat();
}
// 结果:什么都没有输出

此处schrodingers的初始化永远不会被执行,并且对print()的调用也永远不会执行。

4. final变量

Dart的变量可以用单词final作为前缀,表明他们在初始化后不能再修改。必须在声明时就进行初始化。

final origin1 = new Point(0, 0);
class Point1 {
  final x,y;
  Point1(this.x, this.y);
}

5. 相同与相等

所有对象都支持相等操作符==。这个操作符是在Object中定义的,用于检测参数与接收者是否相同。

var aPoint = new Point(3,4);
var anotherPoint = new Point(3,4);
aPoint == anotherPoint; // 值为false

因为每个对象都有唯一标识,一个对象只与它自己相同。我们可以重新 ==

operator == (p)=>x==p.x&&y==p.y;

dart:core库中定义了一个identical()方法,开发者可以使用它来检查两个对象是否相同。

print(identical(origin,origin));
print(identical(aPoint,aPoint));
print(identical(aPoint, anotherPoint));
结果:
true
true
false

我们现在有足够的知识来定义Object类的相等方法:

bool operator ==(other)=>identical(this, other);
···
  print(origin == origin);
  print(aPoint == aPoint);
  print(aPoint == aPoint1);
  print(aPoint == anotherPoint);
结果:
true
true
false
false

6. 类与父类

每个类都声明了一组实例成员,包括实例变量和各种实例方法。每个类(Object除外)继承了父类的实例成员。除了Object类外所有的类都只有一个父类,Object没有父类,所以Dart类层次结构形成了一个以Object类为根的树。这种结构叫做单继承。

7. 抽象方法与抽象类

简单的声明一个方法而不提供它的实现是有用的,这种方法被称为抽象方法。任何种类的实例方法都可以是抽象的,不管是getter,setter,操作符或普通方法。

有一个抽象方法的类本身就是一个抽象类,抽象类的声明是通过在类名前加上前缀abstract。

8. 接口

每个类都隐含的定义了一个接口,此接口描述了类的实例拥有哪些方法。很多编程语言都有正式的接口声明,但在Dart中没有。这是不必要的, 因为我们始终可以定义一个抽象类来描述所需的接口。

abstract class CartesianPoint{
  get x;
  get y;
}
abstract class PolarPoint{
  get rho;
  get theta;
}
class Point implements CartesianPoint, PolarPoint{
  
}

在运行时检查对象时否符合某个接口是可行的

print(5 is int);
  print('x' is String);
  print(aPoint is CartesianPoint);
  print(new Point2() is CartesianPoint);
  print(new Point2() is PolarPoint);
结果:
true
true
false
true
true

请注意,is不检查对象是否为某个类或子类的实例。相反,is检查对象的类是否明确的实现了某个接口(直接或间接)。换句话说,我们并不关心对象是如何实现的,我们只在意它支持哪些接口。这是与其他有类似构造的语言的关键区别。如果一个类希望模拟另一个类的接口,则它并不局限于已有的实现。

接口的继承类似于类。类的隐含接口会继承父类的隐含接口,同时会继承父类实现的接口。同类已有, 接口可以重写父接口的实例方法;另外,某些重写可能是非法的,例如重写方法与被重写方法的参数不一致,或者试图用普通方法重写getter或setter,反之亦然。

另外一个接口有多个父接口,不同的父接口之间可能会产生冲突。假设一个同名的方法在多个父接口中出现,而且它们的参数不一致,则在这种情况下,互相冲突的方法没有一个会被继承,如果一个父类定义了一个getter, 而另一个父类也定义了同名的普通方法,那么结果也是一样的。

9. 对象的创建

Dart中的计算都是围绕对象展开的。因为Dart是纯面向对象的语言,所以即使是最微不足道的Dart程序也会涉及对象的创建。

一个构造函数的函数体在开始前总是隐含的运行父类构造函数的函数体。传递给父构造函数的参数跟初始化列表中的调用父构造函数的参数相同,他们不会重新计算。

9.1 重定向构造函数

重定向构造函数的目的是把执行重定向到另一个构造函数。在重定向构造函数中,参数列表跟在一个冒号后面, 并以this.id(。。。)的形式指定重定向到哪个构造函数。这里是Point.polar()

Point.polar(this.rho, this.theta);
Point(a, b):this.polar(sqrt(a*a+b*b), atan(a/b));

9.2 工厂构造函数

假设我们想要避免分配过多的点。我们想保留点的一份缓存,而不是每次请求都生成一个新的点。当有人尝试分配一个点时,我们就检查缓存中管是否存在相等的点,如果有, 就返回那一个点。

一般来说,构造函数使上诉设想比较难实现。如我们前面描述的,在大多数编程语言中,构造函数总是会分配一份新的实例。如果想使用缓存,那么必须提前考虑好,并确保你的点是通过一个方法来调用分配的,而这个方法通常叫做工厂方法。在Dart中, 任意构造函数都可以被替换工厂方法,并且对客户是完全透明的。我们通过工厂构造函数来做到这一点。

工厂构造函数由factory前缀开头。他们看起来像普通的构造函数,但可能没有初始化列表或初始化形式参数。相反,他们必须有一个返回一个对象的函数体。工厂构造函数可以从缓存中返回对象,或选择分配一个新的实例。它甚至可以创建一个不同类的实例(或者从缓存或其他数据结构中查找他们)。只要生成的对象符合当前类的接口,则一切都会按预期执行。

10. noSuchMethod()

Dart中的计算都是围绕对象方法的调用。如调用了一个不存在的方法, 则默认的行为是抛出NoSuchMethodError错误。但,并非总如此。

11. 常量对象与字段

有些对象是在编译时就可以计算的常量。Dart还支持用户定义的常量对象。常量对象的创建时使用const而不是new。const也是调用构造函数,但该构造函数必须是常量构造函数,且它的参数必须是常量。实际上,Dart要求常量构造函数的参数必须是数字,布尔量或者字符串。

 const origin = const Point(0, 0);

12. 类方法

类方法是不依赖于个体实例的方法。通过类变量而引入的accessor都是类方法,我们可以称他们为类getter和setter。

13. 实例及其类与元类

每个对象都是一个类的实例。既然一切都是对象,那么类也是对象;既然类是对象,那么它们本身也是某个类的实例。类的类通常被称为元类。Dart语言指定类的类型为Type,但没有指明他们属于哪个类。

反射是唯一可靠的发现对象所属类的方式。对象都支持一个名为runtimeType的getter,它默认返回对象的所属类。但是子类可随意重写runtimeType

14. Object与其方法

class Object {
  const Object();
  external bool operator ==(other);
  external int get hashCode;

  external String toString();
  external dynamic noSuchMethod(Invocation invocation);
  external Type get runtimeType;
}

被标记为external,表明他们的实现是在其他地方。external机制用于声明代码的实现来自于外部。这些外部代码可以有多种提供方式, 通过作为底层实现基础的外部函数接口,或者甚至可能动态的生成实现。

15. mixin

单继承有很大的局限性。Dart使用基于mixin的继承。
为了理解基于mixin的继承,我们将考察一个简化版的集合类:

abstract class Collection{
  forEach(f);
  where(f);
  map(f);
}

我们真正想要的是Collection的主体,即大括号之间的部分。此主体包含了类声明本身提供的功能。它是Collection和父类的差异,我们把这个差异叫做mixin。

我们可以把mixin看作一个函数,它接收一个父类s并返回一个新的拥有特定主体的S子类。我们把M与父类S的mixin操作写成S with M。显然,S必须指定一个类。

但是我们如何指定mixin?通过指定一个类,每个类都通过它的主体隐含定义了一个mixin,我们就是用它来对S执行mixin操作。我们现在知道如何在不复制代码的情况下定义CompoundWidget。

class CompoundWidget extends Widget with Collection {
  // ...CompoundWidget的实现
}

CompoundWidget的父类是Widget与Collection,即类Collection与父类Widget在mixin之后产生的一个新的匿名类。

15.1 mixin例子:表达式问题

class Expression {}

class Addition extends Expression {
  var operand1, operand2;
  get eval => operand1.eval + operand2.eval;
}

class Subtraction extends Expression {
  var operand1, operand2;
  get eval => operand1.eval - operand2.eval;
}

class Number extends Expression {
  int val;
  get eval => val;
}

以上实现方式是有问题的。当你想把这些表达式转换为字符串时,你就需要添加另一个方法到原先的层次结构中。类似功能的函数可能有无数个,你的类很快就会变得难以维护和使用。还有一个问题就是不是所有想添加新功能的人都可以访问到原始源代码。

使用mixin可以很好的解决这个问题,下面的Dart代码将展示如何实现。我们从三个初始数据类型开始,只是我们不把它们定义为抽象类,因为他们并不是我们最终要实例化的数据类型。他们被用来定义类型的结构及对应的构造函数。

library abstract_expressions;
abstract class AbstractExpression{}
abstract class AbstractAddition {
  var operand1, operand2;
  AbstractAddition(this.operand1, this.operand2);

}

abstract class AbstractSubtraction {
  var operand1, operand2;
  AbstractSubtraction(this.operand1, this.operand2);

}

abstract class AbstractNumber {
  var val;
  AbstractNumber(this.val);
}

现在,我们定义第一个功能:求职器。我们将通过一组mixin类来做到这一点。

library evaluator;
abstract class ExpressionWithEval{
  get eval;
}
abstract class AdditionWithEval {
  get operand1;
  get operand2;
  get eval => operand1.eval + operand2.eval;

}

abstract class SubtractionWithEval {
  get operand1;
  get operand2;
  get eval => operand1.eval - operand2.eval;
}

abstract class NumberWithEval {
  get val;
  get eval => val;
}

以上求值器完全独立于类型层次结构,注意我们没有导入哪怕一个依赖。我们的客户端应该使用的时机类型是单独定义的:

library expressions;
import 'abstract_expressions.dart';
import 'evaluator.dart';
abstract class Expression = AbstractExpression with ExpressionWithEval;
class Addition = AbstractAddition with AdditionWithEval implements Expression;
class Subtraction = AbstractSubtraction with SubtractionWithEval implements Expression;
class Number = AbstractNumber with NumberWithEval implements Expression;

每个具体的AST(抽象语法树)类型都被定义成一个mixin应用,即用相应的求职器mixin来扩展对应的抽象数据类。我们可以给expressions库提那家一个main函数,用来构建一个简单的表达式树。这是可能的,因为各个AST节点类的父类构造函数都隐含的为他们定义了合成的构造函数。

main() {
  var e = new Addition(new Addition(new Number(4), new Number(2)), 
  new Subtraction(new Number(10), new Number(7)));
print('$e = ${e.eval}');
}
结果:
Instance of 'Addition' = 9

为什么将抽象类型和实际类型分离?expressions库即实际类型的作用是通过mixin应用连接各个组件来定义我们整个系统。abstractExpressions库即抽象类型的作用是定义我们的AST节点的形式。保持他们的独立,使我们再扩展系统时只需对expressions库做修改,无需触碰我们的数据类型的表现形式。

一般的模式是,每个具体类都基于扩展一个定义其数据表示的抽象类,同时用一系列的mixin来代表该数据类型所具备的功能。这种方法之所以有效, 是因为我们为每个类型和功能的组合单独定义了一个mixin。例如,上面的每个eval方法都是在各自的mixin类中定义的。如果我们想增加一种类型,那么我们可以独立添加。下面我们将增加乘法的AST节点:

library multiplication;
abstract class AbstractMultiplication {
  var operand1, operand2;
  AbstractMultiplication(this.operand1, this.operand2); 
}

这个添加操作同样完全独立于原先的类层次结构和已有功能。现在我们还需要定义乘法是如何求值得。我们可以单独定义一个库:

library multiplication_evaluator;
abstract class MultiplicationWithEval {
  get operand1;
  get operand2;
  get eval => operand1.eval * operand2.eval;

}

再次,以上代码是独立的,我们还需要在expressions库中创建相应的具体类。

import 'abstract_expressions.dart';
import 'evaluator.dart';
class Multiplication = AbstractMultiplication with MultiplicationWithEval implements Expression;

然后在main方法中计算:

main() {
  var e = new Multiplication(new Addition(new Number(4), new Number(2)), 
  new Subtraction(new Number(10), new Number(7)));
  print('$e = ${e.eval}');
}
结果:
Instance of 'Multiplication' = 18

所打印的内容信息量比我们想象的药少,因为e的打印使用的是从Object继承来的默认toString实现。为了解决这个问题,我们可以把一个专门的toString实现添加到我们的类层次结构中。

library string_converter;

abstract class ExpressionWithStringConversion {
  toString();
}

abstract class AdditionWithStringConversion {
  get operand1;
  get operand2;
  toString() => '($operand1 + $operand2)';
}

abstract class SubtractionWithStringConversion {
  get operand1;
  get operand2;
  toString() => '($operand1 - $operand2)';
}

abstract class NumberWithStringConversion {
  get val;
  toString() => '$val';
}

abstract class MultiplicationWithStringConversion {
  get operand1;
  get operand2;
  get eval => '($operand1 * $operand2)';
}

再次,我们按照每一个功能,类型的组合定义一种mixin的方式。这一次,我们知道类层次结构涉及了乘法,我们把与它对应的实现与其他实现都放到同一个库中。同样,我们要改进expressions库来整合新的功能。

library expressions;

import 'abstract_expressions.dart';
import 'evaluator.dart';
import 'multiplication.dart';
import 'multiplication_evaluator.dart';
import 'string_converter.dart';
abstract class Expression = AbstractExpression with ExpressionWithEval, ExpressionWithStringConversion;
class Addition = AbstractAddition with AdditionWithEval, AdditionWithStringConversion implements Expression;
class Subtraction = AbstractSubtraction with SubtractionWithEval, SubtractionWithStringConversion implements Expression;
class Number = AbstractNumber with NumberWithEval, NumberWithStringConversion implements Expression;
class Multiplication = AbstractMultiplication with MultiplicationWithEval, MultiplicationWithStringConversion implements Expression;
// main函数不变,但它会打印一个描述更好的树
Instance of 'Multiplication' = ((4 + 2) * (10 - 7))

我们可以根据需要把扩展过程继续下去。只要你想,你就可以添加多种类型、功能,只要像上面一样, 把要使用的类型的最终形式定义成mixin应用。每个类型所对应的每个功能,都通过一个独立的mixin类来定义。添加新功能确实需要修改这些mixin应用,但是这看起来更像调整你的make文件以包括新加的功能或类型(make是c语言开发中常用的一种构建工具)。如果新类型和功能都是独立定义的,那么我们始终可以定义一个mixin吧新功能独立地添加到新类型上,让他们良好的混合在一起。

mixin使类的代码以模块化方式重用,而不依赖于它在类层次结构中的位置。

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