Dart的语法详解系列篇(二)-- 类与函数

版权声明:本文为博主原创文章,未经博主允许不得转载。https://www.jianshu.com/p/44ae73a58ebc

转载请标明出处:
https://www.jianshu.com/p/44ae73a58ebc
本文出自 AWeiLoveAndroid的博客


本文首发在公众号Flutter那些事,未经允许,严禁转载。

前言
Flutter1.0稳定版昨晚的终于发布了。我们为此感到高兴。对于开发者来说,有了稳定版相当于一个定心丸。本文主要介绍Fllutter1.0的一些功能和相关工具。


Flutter系列博文链接 ↓:

工具安装:

Flutter基础篇:

Flutter进阶篇:

Dart语法系列博文链接 ↓:

Dart语法基础篇:

Dart语法进阶篇:


本文代码同步发布在Github: https://github.com/AweiLoveAndroid/Flutter-learning/tree/master/projects/dart_demo

上一篇主要讲了数据类型、运算符、流程语句等,由于文字太多,我就把剩下的内容分开写一篇文章。
这一篇我们讲Dart的类与函数,内容较多,希望大家可以耐心看完。我也是花了很长时间研究的。喜欢的就点个赞,打个赏吧。感谢大家支持。


八、Dart的类与函数

Dart是一种面向对象的语言,具有类和基于mixin的继承。每个对象都是一个类的实例,所有类都来自Object。 基于Mixin的继承意味着虽然每个类(除了Object)只有一个超类,但是类体可以在多个类层次结构中重用。

(一)类的分类

(1)普通类

1).Dart使用class关键字表示一个类,对象具有由函数和数据(分别为方法和实例变量)组成的成员。

我们通过new关键字来创建一个类对象,然后使用(.)调用类里面的变量或者普通(实例,非静态)函数(注:函数和方法意思是一样的。),以便访问该对象的函数和数据。例如类Test里面有一个普通函数tests(),我们可以使用new Test().tests();来调用这个tests()函数,在Dart2里面创建对象时可以省略new关键字。

例如:

class Test{
  void tests(){}
}
void main(){
  new Test().tests();
}
2).要在运行时获取对象的类型,可以使用Object类的runtimeType属性,该属性返回一个Type对象。

如果没有指明具体类型,Dart具有类型推断机制,会自动推断变量类型。

示例如下:

  var a = 10;
  var b = 10.0;
  var c = '10';
  var d = true;
  var e = [12.5,13.1];
  var f = {3:'5',5:'11'};
  var t = new Test();// 这里就直接使用上面那个Test类 不再去重复写这个类了
  print('a 的类型是: ${a.runtimeType}'); // a 的类型是: int
  print('b 的类型是: ${b.runtimeType}'); // b 的类型是: double
  print('c 的类型是: ${c.runtimeType}'); // c 的类型是: String
  print('d 的类型是: ${d.runtimeType}'); // d 的类型是: bool
  print('e 的类型是: ${e.runtimeType}'); // e 的类型是: List<double>
  print('f 的类型是: ${f.runtimeType}'); // f 的类型是: _InternalLinkedHashMap<int, String>
  print('t 的类型是: ${t.runtimeType}'); // t 的类型是: Test
class Test{}
3). Dart和Java一样,使用extends关键字,表示一个类继承另一个类。

使用@override注解声明你要重写的函数,在这个函数内部可以使用super调用重写的这个父类的函数。

实例如下:

  class Test {
    void test() {/*这里省略方法内部的逻辑操作*/}
  // 其他逻辑
}

class TestChild extends Test {
  @override  //@override标注在test()函数上面 表示test()函数是重写父类的。
    void test() {
      super.test();// 调用父类的test()函数
      /*这里省略方法内部的逻辑操作*/
    }
    // 其他逻辑
}

@override是元数据。元数据注解以字符开头@,后跟对编译时常量(如deprecated)的引用或对常量构造函数的调用。元数据可以出现在库,类,typedef,类型参数,构造函数,工厂,函数,字段,参数或变量声明之前以及导入或导出指令之前。您可以使用反射在运行时检索元数据。
所有Dart代码都有两个注解:@deprecated@override
以下是使用@deprecated 注解的示例:

class Television {
  /// _Deprecated: Use [turnOn] instead._
  @deprecated
  void activate() {
    turnOn();
  }

  // Turns the TV's power on.
  void turnOn() {
    //...
  }
}

您可以定义自己的元数据注释。

这是一个定义带有两个参数的@todo注释的示例:

library todo;

class Todo {
  final String who;
  final String what;
  const Todo(this.who, this.what);
}

// 以下是使用@todo注释的示例:
import 'todo.dart';
@Todo('seth', 'make this do something')
void doSomething() {
  print('do something');
}
4).静态变量

使用static关键字修饰的类范围的变量。
静态变量(类变量)对于类范围的状态和常量很有用。静态变量在使用之前不会初始化。

class Test {
  static const num = 16;
  // ···
}

void main() {
  print(Test. num); // 16
}
5).重写操作符

您可以重写下表中显示的运算符。

操作符名称
< + | []
> / ^ []=
<= ~/ & ~
>= * << ==
- % >>

注意:!=不是可重写的运算符。表达式e1 != e2!(e1==e2)的语法糖。
以下是一个重写+和-运算符的类的示例:

void main(){
  final a = Testoperator(2, 3);
  final b = Testoperator(2, 2);
  var num1 = Testoperator(4, 5);
  var num2= Testoperator(0,1);
  print(a + b == num1); // true
  print(a - b == num2); // true
}

class Testoperator {
  final int x, y;

  Testoperator(this.x, this.y);

  Testoperator operator +(Testoperator o) => Testoperator(x + o.x, y + o.y);
  Testoperator operator -(Testoperator o) => Testoperator(x - o.x, y - o.y);

  // Override hashCode using strategy from Effective Java, Chapter 11.
  @override
  int get hashCode {
    int result = 17;
    result = 37 * result + x.hashCode;
    result = 37 * result + y.hashCode;
    return result;
  }

  // 如果重写了 hashCode,应该重写==操作符。
  @override
  bool operator ==(dynamic other) {
    if (other is! Testoperator) return false;
    Testoperator person = other;
    return (person.x == x &&
        person.y == y);
  }
}
6).noSuchMethod()

要在代码尝试使用不存在的方法或实例变量时检测或做出反应,您可以重写noSuchMethod()

void main() {
  TestMethod test = new TestMethod();
  dynamic f = test.foo;
  // Invokes `Object.noSuchMethod`, not `TestMethod.noSuchMethod`, so it throws.
  f(42);
}

class TestMethod {
// 除非你重写noSuchMethod,否则使用不存在的成员会导致NoSuchMethodError
  // Unless you override noSuchMethod, using a
  // non-existent member results in a NoSuchMethodError.
  @override
  void noSuchMethod(Invocation invocation) {
    print('You tried to use a non-existent member: ' +
        '${invocation.memberName}');
  }
dynamic foo();
}

你不能调用未实现的方法,除非以下的某一条是true:

  • 1.接收处有静态类型dynamic
  • 2.接收处定义了一个未实现的方法(abstract也是OK的)的静态类型dynamic,接收器的动态类型的实现与类noSuchMethod() 中的实现不同Object。
    有关更多信息,请参阅非正式 noSuchMethod转发规范

(2)抽象类

1).使用abstract修饰符定义抽象类(无法实例化的类)。抽象类对于定义接口非常有用,通常还有一些实现。如果希望抽象类看起来是可实例化的,请定义工厂构造函数。

抽象类通常有抽象方法。这是一个声明具有抽象方法的抽象类的示例:

// 此类声明为abstract,因此无法实例化
abstract class Test {
  //定义构造函数,字段,方法...
    
  // 抽象方法
  void test();
}
2).隐式接口

每个类都隐式定义一个接口,该接口包含该类的所有实例成员及其实现的任何接口。如果要在不继承B实现的情况下创建支持B类API的A类,则A类应实现B接口。
一个类通过在implements子句中声明它们然后提供接口所需的API来实现一个或多个接口。
例如:

void main() {
  print(sayHello(Person('李四'))); // 你好 张三. 我是 李四.
  print(sayHello(PersonImpl())); // 你好 张三  你知道我是谁吗?
}

// Person类 隐式接口包含hello()
class Person {
  // 在接口中,但是仅在此库中可见。
  final _name;

  // 不在接口中,因为这是一个构造函数
  Person(this._name);

  // 在接口中
  String hello(String who) => '你好 $who. 我是 $_name.';
}

// Person接口的实现
class PersonImpl implements Person {
  get _name => '';

  String hello(String name) => '你好 $name  你知道我是谁吗?';
}

String sayHello(Person person) => person.hello('张三'); 

一个类也可以实现多个接口,例如:
class ZhangSan implements Run,Life {
  //...
}
class Run {}
class Life {}

(3)可调用的类(Callable Class)

更多详情可以查看Dart官网: 在Dart中模拟函数
要允许像函数一样调用Dart类,请实现该call()方法。
在下面的示例中,Test类定义了一个call()方法,它接受三个字符串并连接它们,用空格分隔每个字符串,并附加一个感叹号。

void main() {
    var test = new Test();
    var result = test(166.6665,"Flutter真好玩",672);
print("$result");// 666.666 Flutter真好玩 666
}
class Test {
    // 必须是call函数
    call(double a, String b, int c) => '${a*4} ${b} ${c-6}';
}

(4)枚举类型

1.使用enum关键字声明枚举类型:

例如:enum Color { red, green, blue }

2). 枚举中的每个值都有一个index getter,它返回枚举声明中值的从零开始的位置。例如,第一个值具有索引0,第二个值具有索引1。
assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);
3).要获取枚举中所有值的列表,请使用枚举values常量。
List<Color> colors = Color.values;
assert(colors[2] == Color.blue);
4).您可以在switch语句中使用枚举,如果您不处理所有枚举值,您将收到警告。
var aColor = Color.blue;
switch (aColor) {
  case Color.red:
    print('Red');
    break;
  case Color.green:
    print('Green');
    break;
  default: // 你没有这个 你会看到一个警告
    print(aColor); // 'Color.blue'
}
5).枚举类型的限制条件:
  • 1.你不能在子类,mixin 或者实现枚举。
  • 2.你不能显式实例化枚举。

(5)mixin

更多关于mixin的资料,可以查看 Dart2.1 mixin规范

Mixins是一种在多个类层次结构中重用类代码的方法。

1).要实现 mixin,请创建一个扩展Object的类,并且不声明构造函数。除非您希望mixin可用作常规类,否则请使用mixin关键字而不是class。

例如:

// 声明mixin
// 专家
mixin Expert {
  // 发现和解决难题
  bool solveProblems = false;
  // 精通数据结构和算法
  bool dataStructureAndAlgorithms = false;
  // 会架构设计
  bool architectureDesign = false;
  // 性能优化
  bool performanceOptimization = false;
  // 熟练掌握计算机系统
  bool computerSystem = false;

  void develop() {
    // 娱乐节目
    if (solveProblems) {
      print('发现和解决难题');
    }
    if (dataStructureAndAlgorithms) {
      print('精通数据结构和算法');
    }
    if (architectureDesign) {
      print('会架构设计');
    }
    if (performanceOptimization) {
      print('熟练掌握性能优化');
    }
    if (computerSystem) {
      print('熟练掌握计算机系统');
    }
  }
}

// 特性

// 有效率的
mixin Efficient {
  void getEfficientAttrs() {
    print('有效率的');
  }
}

// 和蔼的
mixin Kind {
  void getKindAttrs() {
    print('和蔼的');
  }
}

// 软件架构师
class SoftwareArchitect {
  SoftwareArchitect() {
    print('软件架构师');
  }
}
2).要使用 mixin,请使用with关键字后跟一个或多个mixin名称。以下示例显示了两个使用mixins的类。
void main() {
  // 姓名:张三
  // 发现和解决难题
  // 精通数据结构和算法
  // 会架构设计
  ACompanySoftwareArchitect architect1 = new ACompanySoftwareArchitect('张三');
  architect1.develop();
  print('====');
  // 姓名:李四
  // 发现和解决难题
  // 精通数据结构和算法
  // 会架构设计
  // 熟练掌握性能优化
  // 熟练掌握计算机系统
  // 有效率的
  // 和蔼的
  BCompanySoftwareArchitect architect2 = new BCompanySoftwareArchitect('李四');
  architect2.develop();
  architect2.getEfficientAttrs();
  architect2.getKindAttrs();
}

// 使用mixin
// A公司的软件架构师,继承自软件架构师,拥有专家的特性。
class ACompanySoftwareArchitect extends SoftwareArchitect with Expert {
  String name;
  ACompanySoftwareArchitect(String name) {
    this.name = name;
    print('姓名:' + name);
    solveProblems = true;
    dataStructureAndAlgorithms = true;
    architectureDesign = true;
  }

  @override
  void develop() {
    super.develop();
  }
}

//  B公司的软件架构师,继承自软件架构师,
class BCompanySoftwareArchitect extends SoftwareArchitect
    with Expert, Efficient, Kind {
  String name;

  BCompanySoftwareArchitect(String name) {
    this.name = name;
    print('姓名:' + name);
    solveProblems = true;
    dataStructureAndAlgorithms = true;
    architectureDesign = true;
    performanceOptimization = true;
    computerSystem = true;
  }
}
3).要指定只有某些类型可以使用mixin。例如,所以你的mixin可以调用它没有定义的方法, 用于on指定所需的超类。

mixin SoftwareDeveloper on ACompanySoftwareArchitect{}


(二)泛型

如果您查看基本数组类型的API文档 List,您会看到该类型实际上是List<E><...>表示法将List标记为 泛型(或参数化)类型 - 具有正式类型参数的类型。按照惯例,大多数类型变量都有单字母名称,例如E,TSKV`.

(1)为什么使用泛型?

类型安全通常需要泛型,但它们比仅允许代码运行有更多好处:

1).正确指定泛型类型可以生成更好的代码。

如果您希望列表只包含字符串,则可以将其声明为List<String>(将其读作“字符串列表”)。这样一来,工具可以检测到将非字符串分配给列表可能是一个错误。
例子:

var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
// 报错 The argument type 'int' can't be assigned to the parameter type 'String'.
names.add(42); 
2).您可以使用泛型来减少代码重复。

泛型允许您在多种类型之间共享单个接口和实现,同时仍然利用静态分析。
例如:创建了一个用于缓存对象的接口:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}

您发现需要此接口针对字符串的做一个缓存,因此您需要创建另一个接口:

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}

如果还有其他更改,就要写很多接口。

class Test1 extends ObjectCache {
  @override
  Object getByKey(String key) {
    return 'object cache';
  }

  @override
  void setByKey(String key, Object value) {
    return null;
  }
}

class Test2 extends StringCache {
  @override
  String getByKey(String key) {
    return 'String cache';
  }

  @override
  void setByKey(String key, String value) {
    return null;
  }
}

泛型可以省去创建所有这些接口的麻烦。你可以创建一个带有类型参数的接口。
示例如下:T是一个占位符,您可以将其视为开发人员稍后定义的类型。

abstract class Cache<T> {
  T getByKey(String key);
}

使用泛型以前的调用方式

print(new Test1().getByKey('123'));
print(new Test2().getByKey('456'));

(2)使用集合文字

listmap文字可以参数化。参数化文字就像你已经看到的文字一样,除了你在开始括号之前添加 <type>(对于list)或 <keyType, valueType>(对于map)。
以下是使用类型文字(typed literals)的示例:

var numbers = <String>['11', '22', '33'];
var pages = <String, String>{
  'index.html': 'Homepage',
  'store.html': 'Store',
  'mine.html': 'Mine'
};

(3)使用带有构造函数的参数化类型

要在使用构造函数时指定一个或多个类型,请将类型放在类名称后面的尖括号<...>中。

例如:

var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
var nameSet = Set<String>.from(names);

以下代码创建一个具有整数的key和View类型的value的map:

var views = Map<int, View>();

(4)泛型集合及其包含的类型

Dart的泛型类型是具体的。也就说,它们在运行时会会携带类型信息。

var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true
print(names.runtimeType); // List<String>

注意:相反,Java中的泛型使用擦除,这意味着在运行时删除泛型类型参数。在Java中,您可以测试对象是否为List,但您无法测试它是否是List<String>

(5)限制参数类型

实现泛型类型时,您可能希望限制其参数的类型。你可以在<>里面使用extends。
例如:

abstract class SomeBaseClass {
    // 其他操作
}

class Foo<T extends SomeBaseClass> {
  String toString() {
      return "Instance of Foo<$T>";
  }
}

class Extender extends SomeBaseClass {
     //其他操作
}

现在可以使用SomeBaseClass或它的任何子类作为泛型参数。

例如:

void main() {
    var someBaseClassFoo = Foo<SomeBaseClass>();
    var extenderFoo = Foo<Extender>();
    print(someBaseClassFoo.toString());// Instance of Foo<SomeBaseClass>
    print(extenderFoo.toString());// Instance of Foo<SomeBaseClass>
}

也可以不指定泛型参数。
例如:

var foo = Foo();

//等同于print(foo.toString());

print(foo);// Instance of Foo<SomeBaseClass>

如果指定任何非SomeBaseClass类型会导致错误。
例如:var foo = Foo<Object>;

(6)使用泛型方法

新版本的Dart的泛型方法,允许在方法和函数上使用类型参数。(但它同样适用于实例方法,静态方法,顶级函数,本地函数甚至lambda表达式。)
例如:
    T first<T>(List<T> data) {
      // 做一些初始工作或错误检查...
      T tmp = data[0];
  // 做一些额外的检查或处理...
  return tmp;
}

在first(<T>)上的的泛型类型参数,允许你在以下几个地方使用类型参数T:

  • 1). 在函数的返回类型(T)中
  • 2). 在参数类型(List<T>)中
  • 3). 在局部变量的类型(T tmp)

泛型方法可以声明类方法(实例和静态)以相同的方式获取泛型参数。

class Test {
  static int f<M, N>(int x) => 3;
  int m<M, N>(int y) => 5;
}

泛型方法也适用于函数类型参数,本地函数和函数表达式。
// 作为参数

void functionTypedParameter<T>(T callback){}

// 声明一个本地泛型函数本身

void  localFunction(){
   T itself<T>(T thing) => thing;
}

// 将泛型函数表达式绑定到局部变量。

void functionExpression(){
   var lambda =  <T>(T thing) => thing;
}

使用泛型方法:

void main() {
      List<String> data = ["张三","李四","王五"];
      var result = first(data);
      print(result);
}

(三)函数

(1)普通函数

Dart是一种真正的面向对象的语言,所以即使是函数也是对象,函数属于Function类型。可以通过函数指定变量或者把函数作为其他函数的参数。

1)函数的简写。

1.对于只有一个表达式的函数,可以简写。

例如flutter新建工程里面的main.dart,找到里面的runApp函数,可以使用 =>这样的箭头函数去操作,如下所示:
操作前:

  void main(){
    runApp(new MyApp());
  }

操作后:(main.dart文件里面默认使用的是==>箭头函数)

void main() => runApp(new MyApp());

【注意:】main函数是程序的入口,不管是纯Dart代码,还是Flutter项目,或者其他语言,基本都是main函数是入口函数。

2.返回值为void时,可以省略void关键字(开发中不建议这么做)。

函数的返回值可以是void,也可以是null,也可以是具体对象。如果没有指定返回值,则该函数返回的是null。例如flutter新建工程里面的main.dart_incrementCounter()函数,可以省略关键字void,如下所示:
操作前:

void _incrementCounter(){
  //...
}

操作后:

_incrementCounter(){
  //...
}

我们使用assert(_incrementCounter()==null);测试一下,发现程序运行正常,可以看出该函数返回值为null

【注意】函数属于Function类型,可以通过断言assert(XXX is Funtion);判断出结果,返回值必须是具体类型或者省略,如果返回值写为void,编译器有错误提示。
举例如下:

void testMethod (){
  //...
}

例如我们:assert(testMethod () is Function);//这时候编译器会报错。

2)普通参数与可选参数

Dart的函数最好玩的就是这个可选参数了,就是可以声明多个参数,使用时可以调用其中的某一个或者多个参数,与参数位置无关。

1.可选参数的基本使用

可选参数的定义方式:{参数1,参数2,,...},使用方式:函数名(paramName1: value1, paramName2: value2, paramName3: value3...);

下面我们来看一个简单的示例对比一下普通函数和可选参数:

操作前:

// 工作:地址、公司名、工资、工作时长、公司人数
void work(
  String address, 
  String cpompany, 
  double money, 
  String workTime,
  int workerNumbers) {
  //TODO:... 
}

使用:

void main() {
  // 缺一个参数都会报错
  work('hangzhou','XXCompany',1000000.00,'9:00-5:00',500);
}

操作后:

void work2({
  String address, 
  String cpompany, 
  double money, 
  String workTime,
  int workerNumbers}) {
  //TODO:... 
}

使用:

void main() {
  //你随意使用其中的参数都是可以的,例如我使用了其中的参数1,参数4和参数5
  work2(address:'hangzhou', workTime:'9:00-5:00', workerNumbers:500);
}

2.可选参数默认的值

可以使用 = 为任意的可选参数设置默认值,默认值必须是编译时常量,如果没有提供默认值,则默认值为null。
例如下例就是给参数1和参数2设置了默认值:

void work3({
  String address = 'hangzhou', 
  String cpompany = ' XXCompany ', 
  double money, 
  String workTime,
  int workerNumbers}) {
    //TODO:... 
}

3.普通函数参数为list或者map的默认值

如果普通函数的参数是一个匿名List集合(也叫数组),也可以使用 = 设置默认值,数组不能被包含在可选参数里面。

例如:

void work4(
    String address,
    [String cpompany = 'XXCompany',
    double money,
    String workTime,
    int workerNumbers]) {
  //TODO:...
}

4.可变参数为list或者map的默认值

可变参数可以是显示声明的List集合或者map,但是list或者map的值比如是const修饰。

举例如下:

void work5({
    List<int> list = const [10, 20, 30],
    Map<String, String> gifts = const {
     'cpompany':'XXCompany',
     'money':'50000',
     'workTime':'9:00-5:00',
    }}) {
  //TODO:...
}
3)函数作为一个参数传给另一个函数

这个就类似于java里面的回调功能。例如flutter新建工程里面的main.dart,我们看看这段代码就知道了:

// 函数作为参数传给另一个函数
void main() {
    // 例如main.dart里面FloatingActionButton的onPressed参数引入了一个_incrementCounter()函数
    // floatingActionButton: new FloatingActionButton(onPressed: _incrementCounter,), 
}

void _incrementCounter() {
//   setState(() {
//     _counter++;
//   });
}
4)匿名函数

我们还是以flutter新建工程里面的main.dart为例,我们看看这里的setState函数,这里面的参数是一个(){}。小括号里面没有参数,我们去看看setState源码你会发现它的参数是一个Function,这里没有传入任何参数。这里面其实就是一种匿名函数的用法。

void _incrementCounter() {
  setState(() {
    _counter++;
  });
}

再比如常见的list.foreach用法也会用到匿名函数,例如下例中的forEach()我们这里写的是无类型参数的匿名函数item,forEach 源码是:forEach (void f(E element)),它的参数是一个函数。

List list = [10, 7, 23];
list.forEach((item) {
  print('$item');
});

以上语句可以简写成:list.forEach((item) => print('$item'));

5)函数作用域

Dart是一种具有语法范围的语言,变量的范围是静态确定的,只需通过代码布局来确定。通过花括号向外查看,可以确定变量是否在范围内。
以下是一个嵌套函数的例子,每个作用域级别上都有变量,变量作用域为函数内部,外部无法访问。我们可以看看日志就清楚了:

// main函数里面可以输出topLevel和insideMain的值。
// myFunction函数里面可以输出topLevel、insideMain和insideFunction的值。
// nestedFunction函数里面可以输出topLevel、insideMain、insideFunction和insideNestedFunction的值。
bool topLevel = true;
void main() {
  var insideMain = true;
  void myFunction() {
    var insideFunction = true;
    void nestedFunction() {
      var insideNestedFunction = true;
      print('topLevel\r');
      print(topLevel);
      print('insideMain\r');
      print(insideMain);
      print('insideFunction\r');
      print(insideFunction);
      print('insideNestedFunction\r');
      print(insideNestedFunction);
    }
    // print('topLevel\r');
    // print(topLevel);
    // print('insideMain\r');
    // print(insideMain);
    // print('insideFunction\r');
    // print(insideFunction);
    // 调用函数nestedFunction
    nestedFunction();
  }
  // 调用函数myFunction
  myFunction();
  // print('topLevel\r');
  // print(topLevel);
  // print('insideMain\r');
  // print(insideMain);
}
6)闭包

当函数定义和函数表达式位于另一个函数的函数体内。而且这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。

  • a. 内部函数为有参数的匿名函数示例:
void main() {
  var result = test();
  print(result(2.0));//结果为:12.56
}

Function test(){
  const PI = 3.14;
  return (double r) => r * r * PI;
}
  • b. 内部函数为无参数的匿名函数示例:
void main() {
  var result2 = test2();
  print(result2());//结果为:3.14
}

Function test2() {
  const PI = 3.14;
  return () => PI;
}
7)等价函数
//函数也是对象

void topMethod() {} // 一个顶级函数

class Demo {
  static void staticMethod() {} //一个静态方法
  void caseMethod() {} //实例方法
}

void main() {
  var compareVar;

  // 比较顶级的函数
  compareVar = topMethod;
  print(topMethod == compareVar);

  // 比较静态方法
  compareVar = Demo.staticMethod;
  print(Demo.staticMethod == compareVar);

  // 比较实例方法
  var demo1 = Demo(); // Demo类的实例1
  var demo2 = Demo(); // Demo类的实例2
  var y = demo2;
  compareVar = demo2.caseMethod;

  //这些闭包指向同一个实例demo2,所以它们相等。
  print(y.caseMethod == compareVar);

  //这些闭包是指不同的实例,所以他们不平等。
  print(demo1.caseMethod != demo2.caseMethod);
}
8)函数别名

在Dart中,函数是对象,就像字符串一样,数字是对象。一个类型定义,或功能型的别名,给出了一个函数类型声明字段时,您可以使用和返回类型的名称。当函数类型分配给变量时,typedef会保留类型信息。

以下代码,它不使用typedef:我们可以看到funs是一个函数,但它是哪一种类型的函数?不是很清楚。

class Demo {
  Function funs;
  Demo (int f(Object a, Object b)) {
    funs = f;
  }
}

int test(Object a, Object b) => 0;

void main() {
  Demo demo = Demo(test);
  // funs是一个函数,但它是哪一种类型的函数?
  print(demo.funs is Function); // true
}

可以使用typedef给函数取个别名,这一点让我想起了C语言里面的函数指针。接下来使用typedef改造一下,我们将代码更改为使用显式名称并保留类型信息,开发人员和工具都可以使用该信息。

  1. 给Function取一个别名叫做TypedefFuns
typedef TypedefFuns = int Function(Object a, Object b);
  1. Demo类里的构造方法使用这个别名
class Demo {
  TypedefFuns funs;
  Demo(this.funs);
}
  1. 使用Demo类,传入一个函数。这里给Demo类传入了一个函数test。如果想知道然后判断demo.funs属于哪一种类型。
int test(Object a, Object b) => 0;

void main() {
  Demo demo = Demo(test);
  print(demo.funs is Function); // true
  print(demo.funs is Demo); // false
}

目前:typedef仅限于函数类型。

因为typedef只是别名,Dart提供了一种检查任何函数类型的方法。
例如:

typedef TypedefFuns2<T> = int Function(T a, T b);
int test2(int a, int b) => a - b;
void main() {
  print(test2 is TypedefFuns2<int>); // True
}
9)getter 和 setter

gettersetter是提供对象属性的读写访问权限的特殊方法。所有实例变量都生成一个隐式getter方法。非final实例变量也会生成隐式setter方法。使用get和set关键字通过实现getter和setter来创建其他属性。

  • 1.使用getter和setter,可以从实例变量开始。

例如:

void main() {
  var rect = Rectangle(3, 4, 20, 15);
  print(rect.left == 3); // true
  rect.right = 12;
  print(rect.left == -8); // true
}

class Rectangle {
  num left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  num get right => left + width;
  set right(num value) => left = value - width;
  num get bottom => top + height;
  set bottom(num value) => top = value - height;
}

注意: 无论是否明确定义了getter,增量(++)等运算符都以预期的方式工作。为避免任何意外的副作用,只需调用一次getter,将其值保存在临时变量中。

  • 2.实例变量的隐式getter和setter方法

所有实例变量都生成一个隐式getter方法。非final实例变量也会生成隐式setter方法。

例如:

class Point {
    num x;
    num y;
}

void main() {
    var point = Point();
    point.x = 4; // Use the setter method for x.
    assert(point.x == 4); // Use the getter method for x.
    assert(point.y == null); // Values default to null.
}

如果初始化声明它的实例变量(而不是构造函数或方法),则在创建实例时设置该值,该实例在构造函数及其初始化列表执行之前。

例如:

class Point2 {
  num x = 10;
  num y = 5;
  Point2 p = new Point2();//p在构造函数之前执行
  Point2(){}
}
void main() {
    var point2 = Point2();
    point2.x = 4; // 
}
10)抽象方法

实例,gettersetter方法可以是抽象的,定义一个接口,但将其实现留给其他类。抽象方法只能存在于抽象类中。要使方法抽象,请使用分号(;)而不是方法体。

abstract class Test {
  //定义实例变量和方法...

//定义一个抽象方法
void doSomething(); 
}

class TestImpl extends Test {
  // 抽象方法的实现
  void doSomething(){
    // 具体的实现...
  }
}
11)静态方法

静态方法:使用static关键字修饰的方法,也叫类方法,它不对实例进行操作,因此无权访问this

void main() {
  print(Point.area(5, 4));// 10
}
class Point {
  static num area(num x, num y) {
    return (x * y)/2;
  }
}

注意:对于常用或广泛使用的实用程序和功能,请考虑使用顶级函数而不是静态方法。您可以使用静态方法作为编译时常量。例如,您可以将静态方法作为参数传递给常量构造函数。

(2)构造函数

通过创建一个与其类同名的函数来声明构造函数(另外,还有一个额外的标识符,如命名构造函数中所述)。

1).最常见的构造函数形式,即生成构造函数,创建一个类的新实例。

例如:

class Test {
  int x, y;

  Test(int x, int y) {
    this.x = x;//this.x指向的是当前Test类里面的变量int x
    this.y = y;
  }
}
void main(){
  // 使用Test构造函数创建Test对象
  var test = new Test(5, 15);
}
2).Dart具有语法糖,可以将构造函数参数赋值给实例变量。
class Test {
  num x, y;
  
  // 构造函数运行之前设置x和y
  Test(this.x, this.y) {
    print('x:$x, y:$y');
  }
}

如果没有内容体,可以使用简写形式,示例如下:
class Test {
  num x, y;
  
  // 构造函数运行之前设置x和y
  Test(this.x, this.y);
}
3).默认构造函数(空参构造)

如果您未声明构造函数,则会为您提供默认构造函数。默认构造函数没有参数,并在超类中调用无参数构造函数。

注意:如果定义了空参构造,再去写实参构造,会报错(这一点和java不一样)。

  class Test{
    // 如果不写 默认就是空参构造
  Test(){}
}
4).命名构造函数

Java可以做到方法重载(也就是:多个同名不同参数构造函数),但是Dart不可以这么做。Dart提供了命名构造。使用命名构造函数为类实现多个构造函数或提供更多的解释说明。
例如:

class Test{
  num x, y;

  // 命名构造
  Test.help() {
    x = 5;
    y = 10;
    print('x=${x}, y = ${y}');
  }
}
void main(){
  Test.help();// 调用命名构造函数
}

构造函数不是继承的,也就是说超类的命名构造函数不会被子类继承。如果希望使用超类中定义的命名构造函数创建子类,则必须在子类中实现该构造函数。
例如:Test类里面有一个Test.help()命名构造,它的子类是TestChild,如果使用new TestChild.help()就会报错,因为构造函数不能继承。只有在TestChild类里面写一个TestChild.help()命名构造函数,才可以使用该命名构造。

示例如下:

void main() {
  // 先执行Test类的空参构造, 再执行TestChild类的空参构造。
new TestChild();
  // 调用时会报错
  //new TestChild.help();
}

class Test{
  var x, y;
  Test(){
    print('这是 Test 类的空参构造');
  }
// 命名构造不能被继承
  Test.help(){
    x = 5;
    y = 10;
    print('Test.help() 命名函数 x=${x}, y = ${y}');
  }
}

class TestChild extends Test{
  var x, y;
  TestChild(){
    print('这是 TestChild 类的空参构造');
  }
  // 加上与父类相同的命名构造就不会错 注释了就会报错
  // TestChild.help(){
  //   x = 3;
  //   y = 2;
  //   print('TestChild.help() 命名函数 x=${x}, y = ${y}');
  // }
}
5).构造函数不是继承的

子类不从其超类继承构造函数。声明没有构造函数的子类只有默认(无参数,无名称)构造函数。

6).构造函数调用流程

默认情况下,子类中的构造函数调用超类的无参构造函数。超类的构造函数在构造函数体的开头被调用。如果 还使用初始化列表,则在调用超类之前执行。
执行顺序如下:
初始化列表 -> 超类的无参数构造函数 -> 主类的无参数构造函数,
超类必须要有一个空参构造,如果超类没有未命名的无参数构造函数,则必须手动调用超类中的一个构造函数。在冒号(:)之后,在构造函数体(如果有)之前指定超类构造函数。
例如下面的示例:TestChild类和其超类Test类。

运行结果为:

Test 空参构造
TestChild 有参构造
面积为:6.0

示例代码:

void main() {
  var result = new TestChild.area(3, 4);
  print('面积为:${result.area}');
}

class Test {
  num width;
  num height;
  num area;

  // 必须加上空参构造,如果注释掉 它的子类会报错
  Test() {
    print('Test 空参构造');
  }

  Test.area(width, height)
      : width = width,
        height = height,
        area = width * height {
    print('Test 有参构造');
  }
}

class TestChild extends Test {

  num width;
  num height;
  num area;

  TestChild() {
    print('TestChild 空参构造');
  }

  TestChild.area(num width, num height)
    : area = (width * height)/2 {
    print('TestChild 有参构造');
  }
}

注意事项:

    1. 在调用构造函数之前会计算超类构造函数的参数,所以参数可以是一个表达式。
void main(){
  new TestChild();
}

class Test{
  static String data;
  Test() {
    print('Test 空参构造,data: $data');
  }

  Test.area(String datas) {
    print('Test.area 命名函数,data: $datas');
  }
}

class TestChild extends Test {
  // 参数可以是一个表达式
  TestChild() : super.area(getDefaultData()) {
    print('TestChild 空参函数 调用父类的命名构造');
  }

  static String getDefaultData(){
    return 'TestChild的数据 getDefaultData';
  }
}

执行结果为:

  Test.area 命名函数,data: TestChild的数据 getDefaultData
  TestChild 空参函数 调用父类的命名构造
  • 2.超类构造参数不能使用this关键字。例如:参数可以调用静态方法,但是不能调用实例方法。
    例如上例中的Test.area,修改一下就会报错:
Test.area(this.width, this.height)
       : width = width,
         height = height,
         area = width * height {
print('Test 有参构造');
}
7).重定向构造函数

有时构造函数的唯一目的是重定向到同一个类中的另一个构造函数。重定向构造函数的主体是空的,构造函数调用出现在冒号(:)之后。

示例如下:

void main() {
  var result = new Test(4, true, '数字', 10);
  print('abcd分别是:${result.a},${result.b},${result.c},${result.d}');
}

class Test {
  num a;
  bool b;
  String c;
  num d;
  // 主构造函数
  Test(this.a, this.b, this.c, this.d);

  // 委托给主构造函数
  Test.test1(num x,bool y) : this(x, y,'', 0);
  Test.test2(num a,bool b, String c) : this(a, b, c, 0);
  Test.test3(num a,bool b, String c,num d) : this(a, b, c, d);
}

结果是:abcd分别是:4,true,数字,10

8).常量构造函数
  • 1.如果您的类生成永远不会更改的对象,则可以使这些对象成为编译时常量。为此,请定义const构造函数并确保所有实例变量都是final。要使用const构造函数创建编译时常量,请将const关键字放在构造函数名称之前。
    示例代码:
void main() {
  var result = new Test(4, 10);
  print('x=${result.x}, y:=${result.y}'); // x=4, y=10
  print(Test.origin.x); // 5
  print(Test.origin.y); // 1

  var a = const Test(1, 1);
  var b = const Test(1, 1);
// 他们是相同示实例
  print(a == b); // true

class Test {
  static final Test origin = const Test(5, 1);
  final num x;
  final num y;
  const Test(this.x, this.y);
}
  • 2.常量上下文中的const构造函数

在常量上下文中,您可以省略const构造函数或文字之前的内容。
常量上下文,可以简单的理解为:const后面包裹的语句一定是连续的一个整体,例如声明一个list或者map。
例如,查看此代码,该代码创建一个const的map:

// 这里有很多const关键字
const pointAndLine = const {
  'point': const [const ImmutablePoint(0, 0)],
  'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};
class ImmutablePoint{
  const ImmutablePoint(int a, int b);
}

您可以省略除const关键字的第一次使用之外的所有内容:

// 只有一个const, 它建立了常量上下文
const pointAndLine2 = {
  'point': [ImmutablePoint(0, 0)],
  'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};

如果一个常量构造函数在常量上下文之外,并且在没有常量的情况下被调用,则会创建一个非常量对象:

var a = const ImmutablePoint(1, 1); //创建一个常量
var b = ImmutablePoint(1, 1); // 不创建常量
assert(!identical(a, b)); // 不是同一个实例
9).工厂构造函数

factory是在实现不总是创建其类的新实例的构造函数时使用关键字。例如,工厂构造函数可能从缓存中返回实例,或者它可能返回子类型的实例。

class Test{
  final String name;
  static Map<String, Test> _cache = new Map<String, Test>();
  factory Test(String name) {
    if (_cache.containsKey(name)) {
      return _cache[name];
    } else {
      final symbol = new Test._internal(name);
      _cache[name] = symbol;
      return symbol;
    }
  }
  Test._internal(this.name);
  void test(){
    print('调用了test()');
  }
}
void main(){
  var a = new Test('abc');
  var b = new Test('abc');
  // 检查两个是否引用的相同的对象
  print(identical(a, b)); // true
  new Test('abc').test();
}

运行结果:

true
调用了test()

说明:

  • 1.工厂构造函数无权访问this
  • 2.可以创建子类的实例(例如:取决于传递的参数)。
  • 3.返回缓存的实例而不是新的实例。
  • 4.可以使用new关键字,也可以不使用。(上例中可以这样写:Test('abc').test())
  • 5.工厂构造函数没有初始化列表(没有 :super())

以下是Dart的工厂函数实现的单例:

// factory实现的单例
class Singleton {
  factory Singleton() => const Singleton._internal_();
  const Singleton._internal_();
}

void main() {
  print(new Singleton() == new Singleton());
  print(identical(new Singleton(), new Singleton()));
}

(3)初始化列表

1).可以在构造函数体运行之前初始化实例变量,用逗号分隔初始化。

例如:

有参构造的初始化:

class Test1 {
  var x, y;
  Test1(var x, var y)
      : x = x,
        y = y {
    print('Test1 有参构造初始化');
  }
}

命名构造的初始化:

class Test2{
  var x,y;
  Test2.from(Map<String, num> json)
      : x = json['x'],
       y = json['y'] {
    print('Test2.from(): ($x, $y)');
  }
}
2). 在实际应用开发中,可以使用assert在初始化列表用来校验输入参数。

例如:

有参构造使用assert校验参数:

class Test1 {
  var x, y;

  Test1(var x, var y) : assert(x >= 0) {
    print('Test1(): ($x, $y)');
  }

}

命名构造使用assert校验参数:

class Test2 {
  var x, y;
  Test2.withAssert(this.x, this.y) : assert(x >= 0) {
    print('Test2.withAssert(): ($x, $y)');
  }
}
3).如果没有更多实际操作内容,可以简写。

有参构造使用assert校验参数的简写形式:

class Test1 {
  var x, y;

  Test1(var x, var y) : assert(x >= 0);

}

命名构造使用assert校验参数的简写形式:

class Test2{
var x, y;
  Test2.withAssert(this.x, this.y)  : assert(x >= 0);
}
4).如果要把构造的初始化和assert校验同时使用,可以采用这种方式:

使用assert简写,然后在内容体里面执行初始化操作。

class Test1{
  var x, y;
  Test1(var x, var y) : assert(x > 0){
    this.x = x;
    this.y = y;
    print('Test1 有参构造初始化');
  }
}
5).设置final字段,初始化程序时更方便。
import 'dart:math';

// 设置final字段,初始化程序时更方便

void main() {
  var p = new Test1(4, 3);
  print('长方形的对角线长度:${p.hypotenuse}');
}

class Test1 {
  final num width;
  final num height;
  final num hypotenuse;

  Test1(width, height)
      : width = width,
        height = height,
        hypotenuse = sqrt(width * width + height * height);
}