面向对象的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。
Dart 也是面向对象的语言,同时支持 mixin 的继承机制。Dart 的每个对象都是一个类的实例,所有的类都继承于 Object 类,基于 mixin 的继承意味着每个类( Object 类除外)只有一个超类(父类),一个类的代码可以在其他多个子类中使用,有过编程经验的人一般都知道这个概念,所以我就不再赘述。
创建类
和大多数语言一样,使用 class
关键字作为前缀创建一个类,使用 extends
继承另外一个类,类中可以包含变量和函数:
class Bar {
const String name = 'bar';
test() => print('test success');
}
class Foo extends Bar {
}
理解对象
使用 new
关键字可以创建一个对象:
void main() {
final foo = new Foo();
print(foo);// output: Instance of 'Foo'
}
Dart 2 以后的版本可以省略构造函数之前的关键字 new
,也就是说下面这种写法和上面的写法是等价的:
void main() {
final foo = Foo();
print(foo);// output: Instance of 'Foo'
}
使用 .
操作符来访问对象的方法或属性:
void main() {
final foo = Foo();
print(foo);// output: Instance of 'Foo'
print(foo.name);// output: bar
foo.test();// output: test success
}
上面的代码中,如果变量 foo
的值为 null
,那么使用 foo.name
访问属性将会报错,而使用 ?.
操作符就可以避免这个问题:
void main() {
var foo;
print(foo?.name);// output: null
}
扩展类
子类使用 extends
关键字继承超类,使用 super
关键字引用超类的变量或函数,并且子类可以覆写超类的实例函数、Getter 和 Setter,例如:
class Person {
final String sex = 'boy';
final int age = 18;
getInfo() => 'name: $sex, age: $age';
}
class Employee extends Person {
final String name = 'leo';
final String position = 'php';
getInfo() {
final info = super.getInfo();
print('$info, name: $name, position: $position');
}
}
main() => Employee().getInfo();// name: boy, age: 18, name: leo, position: php
上面的代码中, Person
类的 getInfo
方法被 Employee
类的 getInfo
方法覆盖了。
我们还可以使用 @override
注解来表明某函数是想覆写超类的同名函数,如果覆写错误,编辑器将会提示错误,用法如下:
class A {
noSuchMethod(Invocation mirror) => print('You tried to use a non-existent member:' + '${mirror.memberName}');
}
void main() {
dynamic a = A();
a.test();// output: You tried to use a non-existent member:Symbol("test")
}
上面的代码中 ,A
类覆写了 Object
类的 noSuchMethod
函数,当调用 A
类中不存在的函数时,会被 A
类中的 noSuchMethod
函数捕获到。
函数
函数是类中定义的方法,是类对象的行为。
构造函数
Dart 的构造函数和 PHP 5 一样,在类中声明一个与类名相同的函数就可以创建一个构造函数。
如果不主动声明构造函数,系统将提供一个默认的构造函数,默认的构造函数是没有名字、没有参数的,并且在被实例化时,系统将自动调用父类的无参数构造函数(若参数都是可选参数,也会被自动调用)。
下面的例子在 Person
类中主动声明了一个构造函数,并初始化 sex
和 age
属性的值:
class Person {
String sex;
int age;
Person({sex = 'girl', age = 18}) {
this.sex = sex;
this.age = age;
}
}
上面的代码中 this
指向 Person
实例,使用 this
关键字可以访问 Person
类中的属性和方法。
由于将构造函数的参数赋值给属性这种写法非常频繁,所以 Dart 提供了语法糖,让我们可以更简洁地实现上面的功能:
class Person {
String sex;
int age;
Person({this.sex = 'girl', this.age = 18});
}
命名构造函数
通过命名构造函数可以让一个类拥有多个构造函数,提供更有针对性的构造函数。
命名构造函数的命名格式为 ClassName.identifier
,用法如下:
class Person {
String sex;
int age;
Person({this.sex = 'girl', this.age = 18});
Person.baby({this.sex = 'girl', this.age = 3});
}
上面的代码中,Person
类提供了 baby
命名构造函数,我们使用 Person.baby()
即可创建一个新的实例:
main() {
final baby = Person.baby();
print(baby.sex);// output: girl
print(baby.age);// output: 3
}
调用超类构造函数
子类不会继承超类的构造函数,默认情况下,子类的构造函数会在执行之前自动调用超类的无参数构造函数(若超类的构造函数都是可选参数,也会被子类自动调用)。
超类若定义了必须传递参数的构造函数,子类的构造函数则不会自动调用超类的构造函数,并且子类必须手动调用超类的构造函数。
例如下面的代码中, Person
类声明了 2 个构造函数,第一个构造函数规定必须传递 2 个参数:
class Person {
final sex;
final age;
Person(this.sex, this.age);
Person.baby({this.sex = 'girl', this.age = 3});
}
接着,再定义一个 Employee
类,继承 Person
类,此时 Employee
类就必须手动调用 Person
构造函数。使用 :
操作符和 super
关键字即可进行手动调用:
class Employee extends Person {
final name;
final salary;
Employee({this.name = 'leo', this.salary = 3000}) : super('girl', 18);
}
main() {
final people = Employee();
print(people.name);// output: leo
print(people.salary);// output: 3000
print(people.sex);// output: girl
print(people.age);// output: 18
}
如果想调用 Person
类的 baby
命名构造函数,使用 super.baby()
即可调用:
class Employee extends Person {
final name;
final salary;
Employee({this.name = 'leo', this.salary = 3000}) : super.baby();
}
main() {
final people = Employee();
print(people.name);// output: leo
print(people.salary);// output: 3000
print(people.sex);// output: girl
print(people.age);// output: 3
}
initialize list(初始化列表)
构造函数后的 :
后面,除了可以使用 super
关键字调用超类的构造函数,还可以初始化实例参数。
多个表达式使用 ,
分隔即可,这些表达式被称之为初始化列表,下面是使用示例:
class Person {
final sex;
final age;
Person(this.sex, this.age);
Person.baby({this.sex = 'girl', this.age = 3});
}
class Employee extends Person {
final name;
final salary;
final position;
Employee(Map people)
: name = people['name'],
salary = people['salary'],
position = people['position'],
super.baby();
}
main() {
final people = Employee({
'name': 'leo',
'salary': 3000,
'position': 'php'
});
print(people.name);// output: leo
print(people.salary);// output: 3000
print(people.position);// output: php
print(people.sex);// output: girl
print(people.age);// output: 3
}
如果在初始化参数列表中使用 super
关键字调用父类的构造函数,推荐将它放到列表的最后,详情参考 Dart 最佳实践
构造函数执行顺序
如果提供了初始化列表,则初始化列表会在超类构造函数执行之前被调用,下面是构造函数的执行顺序:
(1)initialize list(初始化列表)
(2)super class’s no-arg constructor(超类的无参数构造函数)
(3)main class’s no-arg constructor(主类的无参数构造函数)
重定向构造函数
顾名思义,即调用该构造函数会重定向到其他构造函数。重定向函数是没有代码的,使用 :
与 this
调用其他构造函数:
class Person {
final sex;
final age;
Person(this.sex, this.age);
Person.baby(sex) : this(sex, 1);
}
void main() {
final leo = Person.baby('girl');
print(leo.sex);// output: girl
print(leo.age);// output: 1
}
常量构造函数
当一个类的所有变量都使用 final
声明时,我们可以为这个类定义常量构造函数:
class Person {
final sex;
final age;
const Person(this.sex, this.age);
}
上面的代码中,Person
函数就是一个常量构造函数,常量构造函数在被实例化时可以使用 const
声明。
使用 const
声明的对象如下特点:
当有多个变量都实例化了 Person
对象时,如果传入的参数一样时,内存中就只会引用同一个实例,例如:
void main() {
const leo = Person('boy', 18);
const leo2 = Person('boy', 18);
print(leo == leo2);// output: true;
}
下面这种写法是和上面等价的:
void main() {
final leo = const Person('boy', 18);
var leo2 = const Person('boy', 18);
print(leo == leo2);// output: true;
}
如果你不使用 const
关键字,那么实例化后的对象将是两个不同的对象:
void main() {
final leo = Person('boy', 18);
final leo2 = Person('boy', 18);
print(leo == leo2);// output: false;
}
工厂构造函数
当我们实例化构造函数时每次都会创建一个新的对象,如果我们希望并不是每次都创建新对象,则可以使用 factory
关键字来声明一个工厂构造函数。
工厂构造函数与普通构造函数不同,工厂构造函数不会自动生成实例,而是通过代码来决定返回的实例对象,例如:
class Logger {
final String name;
bool mute = false;
static final Map<String, Logger>_cache = <String, Logger>{};
factory Logger(String name) {
if (_cache.containsKey(name)) return _cache[name];
final logger = Logger._internal(name);
_cache[name] = logger;
return logger;
}
Logger._internal(this.name);
void log(String msg) {
if (!mute) print(msg);
}
}
上面的代码中,Logger
类拥有一个工厂构造函数,该函数每次都会先判断私有变量 _cache
中是否存在相同键名的实例,如果存在就会直接返回该实例,不存在就会生成一个新的实例,并将实例缓存至 _cache
中,后面的访问都会从缓存中取出同键名的实例。
调用 Logger
类:
void main() {
final logger = Logger('UI');
final logger2 = Logger('UI');
logger.log('Button clicked');// output: Button clicked
logger2.log('Button clicked');// output: Button clicked
print(logger == logger2);// output: true
}
从输出结果可以看出,Logger('UI')
虽然被调用了 2 次,但是它们其实返回的是同一个实例。
工厂构造函数类似于 static
静态成员,无法访问 this
指针,所以在工厂构造函数中能处理的任务较为有限,一般都会定义一个命名构造函数用于生产实例,例如上面例子中的 _internal
函数就是提供给工厂函数的,用于生产 Logger
实例。
实例函数
对象的实例函数可以访问 this
指针,例如:
class Person {
final String name;
final String sex;
final int age;
Person({this.sex = 'boy', this.age = 18, this.name = 'leo'});
String info() {
return "${this.name}'s age is ${this.age}, sex is ${this.sex}";
}
}
void main() {
final employee = Person();
print(employee.info());// output: leo's age is 18, sex is boy
}
静态变量与静态函数
在类中的变量或函数名前使用 static
关键字作为前缀,即可声明静态变量和静态函数,调用格式为 ClassName.identifier
。
静态变量在第一次使用时才会被初始化,下面是使用静态变量的示例:
class Color {
static const green = const Color('#008000');
final String code;
const Color(this.code);
}
main() => print(Color.green.code);// output: #008000
静态函数也是在第一次使用时才会被初始化,它不在类的实例上执行,所以无法访问 this
,并且调用静态函数并不会执行构造函数,例如:
class Person {
Person() {
print("Person Class is init");
}
static String info() {
return "leo's height is 180cm";
}
}
main() => print(Person.info());// output: leo's height is 180cm
Getter 和 Setter 函数
和其他面向对象的语言一样,Dart 也具有重载功能,就是利用 Getter 和 Setter 来实现。
Getter 和 Setter 是设置和访问对象属性的特殊函数,每个实例变量在底层都具有一个 Getter,如果变量不是 final
或 const
所声明的,则还具有一个 Setter。
使用 get
和 set
关键字即可为不可访问的属性定义 Getter 和 Setter:
class Person {
String name;
String familyName;
Person(this.name, this.familyName);
String get fullName => '${this.name} ${this.familyName}';
set fullName(String value) {
final nameMap = value.split(' ');
this.name = nameMap[0];
this.familyName = nameMap[1];
}
}
void main() {
final singer = Person('B.B.', 'King');
print(singer.fullName);// output: B.B. King
singer.fullName = 'Taylor Swift';
print(singer.name);// output: Taylor
print(singer.familyName);// output: Swift
}
上面的代码中,在读取 singer
实例的 fullName
属性时,会调用 get
,在设置 fullName
属性时,会自动调用 set
。
覆写操作符的函数
Dart 中类的操作符是可以被覆写的,使用 operator
关键字与指定的操作符,即可为该操作符定义执行的逻辑,例如:
class Vector {
final int x;
final int y;
const Vector(this.x, this.y);
Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
Vector operator -(Vector v) => Vector(x - v.x, y - v.y);
}
void main() {
final foo = Vector(1, 2);
final bar = Vector(3, 4);
final foz = foo + bar;
final baz = foo - bar;
print(foz.x);// output: 4
print(foz.y);// output: 6
print(baz.x);// output: -2
print(baz.y);// output: -2
}
上面的代码中,Vector
类中定义了 +
操作符和 -
操作符的执行函数,在对 Vector
的实例 foo
进行加减运算时,实例 bar
会作为形参被传入相应的函数中,最后会返回一个新的 Vector
实例。
call 函数
call 函数是一个很有趣的东西,它可以让我们以调用方法的形式去调用一个类,相关用法如下:
class Person {
call(String firstName, String lastName) => print('Hi, $firstName $lastName');
}
void main() {
final singer = Person();
singer('B.B.', 'King');// output: Hi, B.B. King
}
可以看出,call
函数其实就是一个别名功能,下面的代码和上面是等价的:
void main() {
final singer = Person();
singer.call('B.B.', 'King');// output: Hi, B.B. King
}
访问控制(可见性)
Dart 类中所有变量和函数默认是 public
对外公开的,也就是说在任何地方都可以访问,我们可以通过在变量名或函数名的前面添加 _
前缀将其私有化,被私有化的变量和函数只能被其定义所在的库中访问(每个 Dart app 都是一个库)。
Mixin
Mixin 是一种代码复用机制,它可以使一个类继承多个类,功能类似于 PHP 的 trait
。
使用 with
关键字即可使用此功能,例如:
class Person {
String sex = 'man';
}
class Musical {
bool canPlayPiano = false;
}
class Maestro extends Person with Musical {
Maestro() {
print(sex);
print(canPlayPiano);
}
}
main() => Maestro();// output: man false
上面的代码中,Maestro
类继承了 Person
类,还复用了 Musical
类,如果你需要复用更多的类,使用 ,
隔开即可:
class Maestro extends Person with Musical, Aggressive {
}
枚举类
枚举类型通常称之为 Enumerations 或 Enums,它是一种特殊的类,用来表现一个固定数目的常量,它不能被继承或实现、不能使用 mixin 以及不能被实例化。
使用 enum
关键字可以定义枚举类,例如:
enum Color {
red,
green,
blue
}
枚举类中的每个值都有一个 index
的 Getter 函数,该函数返回当前值在枚举类中的位置,和数组的索引类似(即下标从 0 开始自增),例如:
void main() {
print(Color.red.index);// output: 0
print(Color.green.index);// output: 1
print(Color.blue.index);// output: 2
}
每个枚举类都有一个 values
常量,访问此常量会返回一个包含当前枚举类全部值的 List
,例如:
main() => print(Color.values);// output: [Color.red, Color.green, Color.blue]
我们可以使用 switch
语句来判断枚举类的值,需要注意的是我们必须在 case
语句中处理枚举类的全部值,否则编辑器会提示错误,例如:
void main() {
final blue = Color.blue;
switch(blue) {
case Color.blue:
print('is blue');
break;
case Color.green:
print('is green');
break;
/*default:
print('is red');
break;*/
}
}
抽象类
使用 abstract
修饰符可以定义抽象类,和其他面向对象的语言一样,抽象类是不能被实例化的,只能使用 extends
关键字进行继承。
抽象类中一般包含抽象函数,抽象函数即没有内容的空函数,由子类来实现抽象函数,下面是一个具有抽象函数的抽象类的示例:
abstract class Book {
void updateArticle();
}
上面的代码中,Book
类是不能被实例化的,需要由子类来实现 updateArticle
方法:
class Article extends Book {
void updateArticle() {
print('updated');
}
}
main() => Article().updateArticle();// output: updated
隐式接口
Dart 中的每个类都隐式地定义了包含所有实例成员的接口,并且每个类都实现了自己的接口。
如果我们想使用 A
类来支持 B
类的接口,而又不想继承 B
类的实现,则可以对 A
类使用 implements
关键字来实现 B
类即可。
class Person {
final String _name;
Person(this._name);
greet(who) => print('Hello $who. I am $_name');
}
class Imposter implements Person {
final String _name = '';
greet(who) => print('Hi $who. Do you know who I am?');
}
main() => Imposter().greet('leo');// output: Hi leo. Do you know who I am?
上面的代码中,Imposter
类只实现了 Person
类的接口,如果需要实现更多的接口,则使用 ,
连接,例如:
class Imposter implements Person, Employee, Student {
// ...
}