学Flutter就和学iOS一样,先学基本语言语法的使用,再学习搭UI,iOS是OC和Swift,Flutter么就是Dart,语言学多了语法都差球不多,特别是这些新的语言,很多东西都是相似的,但是长时间不用容易忘记,与其每次去网上瞎jb找,不如自己总结一些,以后看自己写的东西就行了。文中个别结论是我自己总结出来的,不能保证准确性,看到的同学仅供参考。
本章提纲:
1.类和对象
2.构造函数
3.mixin
类的定义
官网对Dart中的类说的很明确了,基本能看到的东西都是class。
- Everything you can place in a variable is an object, and every object is an instance of a class. Even numbers, functions, and
null
are objects. All objects inherit from the Object class.
构造函数
构造函数的名字可以是 ClassName 或者 ClassName.identifier,当类中没有明确指定构造方法时,将默认拥有一个无参的构造方法。这个和Swift中的类一样,但Swift的结构体会默认提供一个带参的构造函数。从Dart2开始,创建对象时new
关键字可以省略。
在类的方法中访问成员变量可以用this
,和OC中的self
一样,但是在没有同名外部变量时this
可以省略。
//定义一个Person类
class Person {
String name;
eat() {
print('$name在吃翔');
}
}
main(List<String> args) {
// 1.创建类的对象
var p = new Person(); // 直接使用Person()也可以创建
// 2.给对象的属性赋值
p.name = 'tong';
// 3.调用对象的方法
p.eat();
}
1.Use ?.
instead of . to avoid an exception when the leftmost operand is null:
这个用法和Swift中的option是不是很相似,都是更加安全的用法,直接看英文,不翻译了。
// If p is non-null, set its y value to 4.
p?.y = 4;
2.Dart为最常用的给属性赋值的构造函数提供了一种便捷的语法糖方式。
Person(String name, int age) {
this.name = name;
this.age = age;
}
// 等同于
Person(this.name, this.age);
命名构造函数
使用命名构造函数可为一个类实现多个构造函数, 也可以使用命名构造函数来更清晰的表明函数意图,就是一般常用的自定义的构造函数。
class Point {
num x, y;
Point(this.x, this.y);
// 命名构造函数
Point.origin() {
x = 0;
y = 0;
}
}
1.初始化列表
可以在构造函数体执行之前初始化实例变量, 各参数的初始化用逗号分隔。基本语法是在构造方法后面加一个(:
),后面跟上需要初始化的参数,以逗号分隔。有个注意点:在初始化列表中是没有办法访问this的。
// 在构造函数体执行之前,
// 通过初始列表设置实例变量。
Point.fromJson(Map<String, num> json)
: x = json['x'],
y = json['y'] {
print('In Point.fromJson(): ($x, $y)');
}
初始化列表还有一个场景,就是当类的成员变量是final类型的时候,用普通的构造函数是没法为变量赋值的,这个时候需要用初始化列表的方式。
class Point {
final num x;
final num y;
final num distanceFromOrigin;
Point(x, y)
: x = x,
y = y,
distanceFromOrigin = sqrt(x * x + y * y);
// Point(this.x, this.y) {
// distanceFromOrigin = sqrt(x * x + y * y);
// }
}
可以使用 assert 来验证输入的初始化列表。
Point.withAssert(this.x, this.y) : assert(x >= 0) {
print('In Point.withAssert(): ($x, $y)');
}
简单的分析一下初始化列表这个东西,Dart这个初始化列表应该是借鉴了C++的初始化列表,第一次接触我有点懵,除了感觉初始化常量的时候有点用之外,对于‘’他优先于构造方法之前执行”这个特性还不是很理解,对于这个特性其实让我想起来了Swift中的两段式构造方式,稍微来回顾一下:
第一个阶段,确保当前类和父类所有存储属性都被初始化。当每个存储型属性的初始值被确定后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步定制它们的存储型属性。
class Man {
var name:String
//指定构造方法
init(name:String) {
self.name = name
}
}
class SuperMan: Man {
var age:Int
init(age:Int) {
print("SuperMan第一阶段开始")
//对子类引入的属性初始化
self.age = age
//代码会报错, 因为调用self.name之前还没有对父类的name进行初始化
//即便在这个地方修改, 也会被后面的初始化语句覆盖
// if age > 30 {
// self.name = "hjq"
// }
//对父类引入的属性进行初始化
super.init(name: "han")
print("SuperMan第二阶段开始")
if age > 30 {
self.name = "hello xiaohange"
}
}
}
看了一下C++也有这种类似的两段式初始化过程:
初始化阶段和计算阶段,初始化阶段先于计算阶段。
初始化阶段 ,所有类类型(class type)的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中.
计算阶段 ,一般用于执行构造函数体内的赋值操作。
这么设计的好处就是可以防止属性在对象被初始化前访问,更加安全,说了这么多我觉得初始化列表也是用来在对象初始化之前先初始化一些东西。
另外看其他文章说使用初始化列表还有一个好处主要是性能问题:
对于内置类型,如int, float等,使用初始化类表和在构造函数体内初始化差别不是很大,但是对于类类型来说,最好使用初始化列表它会比在函数体内初始化派生类成员更快,这是因为在分配内存后,在函数体内又多进行了一次赋值操作,这对于数据密集型的类来说,是非常高效的。
这个好处后期在慢慢体会吧,暂时还理解不了。
构造函数调用顺序
默认情况下,子类不会继承父类的构造函数,但会自动调用父类的默认构造函数(匿名,无参数)。 父类的构造函数在子类构造函数体开始执行的位置被调用,如果父类中没有匿名无参的构造函数, 则需要手工在当前构造函数冒号 (:
) 之后,函数体之前,声明调用父类构造函数。 如果提供了一个 initializer list (初始化参数列表), 则初始化参数列表在父类构造函数执行之前执行。 总之,执行顺序如下:
- initializer list (初始化参数列表)
- superclass’s no-arg constructor (父类的无名构造函数)
- main class’s no-arg constructor (主类的无名构造函数)
通过几个例子来讲的更明白一点:
1.父类和子类都只有一个默认的无参构造函数,子类会默认调用父类的无参构造函数。
class Person {
Person(){
print('default person');
}
}
class Employee extends Person { }
main() {
var emp = Employee();
}
//打印default person
2.父类只有一个默认无参构造函数,子类有一个命名构造函数,子类的命名构造函数会调用父类的无参构造函数。
class Person {
Person(){
print('default person');
}
}
class Employee extends Person {
Employee.fromJson(Map data) {
print('fromJson Employee');
}
}
main() {
var emp = Employee.fromJson({});
// var emp = Employee(); 调用会报错,子类不会继承父类的构造方法
// 打印 default person
// fromJson Employee
}
3.父类只有一个命名构造函数,子类有一个默认无参构造函数,这个时候因为子类要去调用父类的无参构造函数,而父类没有,所以该场景不管子类用何方式初始化都会报错。
class Person {
Person.fromJson(Map data) {
print('fromJson Person');
}
}
class Employee extends Person {
Employee();
}
main() {
// var emp = Employee.fromJson({});调用会报错,子类不会继承父类的构造方法
// var emp = Employee(); 调用也会报错,'Person', has no unnamed constructor
// that takes no arguments.
}
父类必须也提供一个无参构造函数供子类调用
class Person {
Person(){
print('default person');
}
Person.fromJson(Map data) {
print('fromJson Person');
}
}
class Employee extends Person {
Employee();
}
main() {
// var emp = Employee.fromJson({});调用会报错,子类不会继承父类的构造方法
var emp = Employee();
// 打印 default person
}
4.父类只有一个命名构造函数,子类也想使用这个父类的命名构造函数,这个时候有两种方式,要么父类提供一个无参构造函数供子类调用;
class Person {
Person(){
print('default person');
}
Person.fromJson(Map data) {
print('fromJson Person');
}
}
class Employee extends Person {
Employee.fromJson(Map data) {
print('fromJson Employee');
}
}
main() {
var emp = Employee.fromJson({});
// var emp = Employee(); 调用会报错
// 打印 default person
// fromJson Employee
}
要么子类在实现这个同名命名构造函数的的时候在初始化列表里显示调用父类的命名构造函数:
class Person {
// 无参构造函数可有可无
// Person(){
// print('default person');
// }
Person.fromJson(Map data) {
print('fromJson Person');
}
}
class Employee extends Person {
Employee.fromJson(Map data) : super.fromJson(data) {
print('fromJson Employee');
}
}
main() {
var emp = Employee.fromJson({});
// var emp = Employee(); 调用会报错
// 打印 fromJson Person
// fromJson Employee
}
排列组合基本就是这几种情况,以后搞不清了可以回来再看看,估计这块也可以搞成一道面试题。
常量构造函数
在某些情况下,传入相同值时,我们希望返回同一个对象,这个时候,可以使用常量构造方法。
默认情况下,创建对象时,即使传入相同的参数,创建出来的也不是同一个对象,看下面代码:
- 这里我们使用identical(对象1, 对象2)函数来判断两个对象是否是同一个对象:
main(List<String> args) {
var p1 = Person('why');
var p2 = Person('why');
print(identical(p1, p2)); // false
}
class Person {
String name;
Person(this.name);
}
如果将构造方法前加const
进行修饰,那么可以保证同一个参数,构造两个相同的编译时常量会产生一个唯一的,标准的实例:
main(List<String> args) {
var p1 = const Person('why');
var p2 = const Person('why');
print(identical(p1, p2)); // true
}
class Person {
final String name;
const Person(this.name);
}
常量构造方法有一些注意点:
注意一:拥有常量构造方法的类中,所有的成员变量必须是final
修饰的。
注意二:如果是将结果赋值给const
修饰的标识符时,const
可以省略。
重定向构造函数
有时构造函数的唯一目的是重定向到同一个类中的另一个构造函数。 重定向构造函数的函数体为空, 构造函数的调用在冒号 :
之后。
class Point {
num x, y;
// 类的主构造函数。
Point(this.x, this.y);
// 指向主构造函数
Point.alongXAxis(num x) : this(x, 0);
}
工厂构造函数
当执行构造函数并不总是创建这个类的一个新实例时,则使用 factory
关键字。 例如,一个工厂构造函数可能会返回一个 cache 中的实例, 或者可能返回一个子类的实例。后面讲到抽象类的时候就会用到工厂构造方方法。
main(List<String> args) {
var p1 = Person('why');
var p2 = Person('why');
print(identical(p1, p2)); // true
}
class Person {
String name;
static final Map<String, Person> _cache = <String, Person>{};
factory Person(String name) {
if (_cache.containsKey(name)) {
return _cache[name];
} else {
final p = Person._internal(name);
_cache[name] = p;
return p;
}
}
Person._internal(this.name);
}
提示: 工厂构造函数无法访问 this。
抽象类
使用abstract
修饰符来定义抽象类 — 抽象类不能实例化,如果希望抽象类能够被实例化,那么可以通过定义一个 工厂构造函数 来实现,抽象类中的抽象方法必须被子类实现。抽象类这个东西在OC里没有明确的关键字可以标明,只能通过人为定义一套规则来实现,这个我感觉是OC比较古老的地方之一。
// 这个类被定义为抽象类,
// 所以不能被实例化。
abstract class AbstractContainer {
// 定义构造函数,字段,方法...
void updateChildren(); // 抽象方法。
}
方法
1.setter和getter
在方法名之前加上set和get。
main(List<String> args) {
final d = Dog("黄色");
d.setColor = "黑色";
print(d.getColor);
}
class Dog {
String color;
String get getColor {
return color;
}
set setColor(String color) {
this.color = color;
}
Dog(this.color);
}
2.抽象方法
实现抽象类的抽象方法时加上@override
关键字,这个关键字OC也没有,这样在编译器层面就不好保持父类和子类方法名的一致性,比如我父类的方法名改了,子类不知道或者忘了没改,这样在编译期间冇问题,一运行就挂了,这个我觉得是OC古老的地方之二。
abstract class Shape {
getArea();
}
class Circle extends Shape {
double r;
Circle(this.r);
@override
getArea() {
return r * r * 3.14;
}
}
3.noSuchMethod()
当代码尝试使用不存在的方法或实例变量时, 通过重写 noSuchMethod()
方法,来实现检测和应对处理。这货还有几个特殊的使用场景,先不写了,后期实战的时候遇到了再补充。
class A {
// 如果不重写 noSuchMethod,访问
// 不存在的实例变量时会导致 NoSuchMethodError 错误。
@override
void noSuchMethod(Invocation invocation) {
print('You tried to use a non-existent member: ' +
'${invocation.memberName}');
}
}
隐式接口
Dart中的接口比较特殊, 没有一个专门的关键字来声明接口。
默认情况下,定义的每个类都相当于默认也声明了一个接口,可以由其他的类来实现(因为Dart不支持多继承),实现这个接口就可以使用类中的属性和方法。
abstract class Runner {
eat();
}
abstract class Flyer {
la();
}
class SuperMan implements Runner, Flyer {
@override
eat() {
print('超人在吃翔');
}
@override
la() {
print('超人在拉翔');
}
}
为类添加功能: Mixin
某些情况下,一个类可能希望直接使用已有类的原有实现方案,怎么做呢?,在通过implements实现某个类时,类中所有的方法都必须被重新实现,这个其实相当于实现了一个接口,接口的方法必须实现。使用继承吗?但是Dart只支持单继承,那么意味着你只能使用一个类的实现。Dart提供了另外一种方案: Mixin混入的方式,通过创建一个继承自 Object 且没有构造函数的类,来实现 一个 Mixin,如果 Mixin 不希望作为常规类被使用,使用关键字 mixin
替换 class
。 例如:
mixin Runner {
run() {
print('在奔跑');
}
}
mixin Flyer {
fly() {
print('在飞翔');
}
}
// implements的方式要求必须对其中的方法进行重新实现
// class SuperMan implements Runner, Flyer {}
class SuperMain with Runner, Flyer {
}
main(List<String> args) {
var superMan = SuperMain();
superMan.run();
superMan.fly();
}
指定只有某些类型可以使用的 Mixin - 比如,Mixin 可以调用 Mixin 自身没有定义的方法 - 使用 on
来指定可以使用 Mixin 的父类类型:
mixin MusicalPerformer on Musician {
// ···
}
类变量和方法
使用 static
关键字实现类范围的变量和方法。这个和其他语言基本一致,就是OC比较特别,有的时候发现其实第一门语言学OC后面学其他语言的时候不如第一门语言学别的语言来的快,OC很多地方确实比较独特,没有通用性。
class Queue {
//静态变量
static const initialCapacity = 16;
//静态方法
static num distanceBetween(Point a, Point b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
}
void main() {
assert(Queue.initialCapacity == 16);
}