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

96
AWeiLoveAndroid 595a1b60 08f6 4beb 998f 2bf55e230555
6.5 2018.12.30 19:03* 字数 5297

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

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


Flutter系列博文链接 ↓:

工具安装:

Flutter基础篇:

Flutter进阶篇:

Dart语法系列博文链接 ↓:

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


八、Dart的类与函数

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

(一)使用类成员

对象具有由函数和数据(分别为方法和实例变量)组成的成员。调用方法时,使用点(.)来引用实例变量或方法,可以在对象上调用它:可以访问该对象的函数和数据。

var p = Point(2, 2);
// 设置实例变量y的值
p.y = 3;

// 当最左边的操作数为null时,使用?.而不是.避免异常:
// 如果p为非null,则将其y值设置为4. 
// p?.y = 4 ;

// 获取y的值
assert(p.y == 3);
// 调用p的distanceTo() 方法
num distance = p.distanceTo(Point(4, 4));

(二)使用构造函数

您可以使用构造函数创建对象。

(1)构造函数名称可以是类名或 类名.

例如,以下代码使用Point()构造和Point.formJson()构造函数来创建Point对象。

// Point构造函数创建Point对象
var p1 = new Point(2, 2);
// Point.formJson()构造函数创建Point对象
var p2 = new Point.fromJson({'x': 1, 'y': 2});
(2)在Dart2中,创建对象时,new关键字可以省略。
(3)编译时常量构造函数

要使用常量构造函数创建编译时常量,请将const关键字放在构造函数名称之前。

var a = const Test(1, 1);
var b = const Test(1, 1);
// 他们是相同示实例
assert(Test(a, b));
(4)常量上下文中的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)],
};

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

// 只有一个const, 它建立了常量上下文
const pointAndLine = {
  '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)); // 不是同一个实例

(三)获取对象的类型

要在运行时获取对象的类型,可以使用Object的runtimeType属性,该属性返回一个Type对象。
例如:

  class Test{}
  var a = 10;
  var b = 10.0;
  var c = '10';
  var d = true;
  var e = [1,2];
  var f = {3:'5',5:'11'};
  var t = new 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<int>
  print('f 的类型是: ${f.runtimeType}'); // f 的类型是: _InternalLinkedHashMap<int, String>
  print('t 的类型是: ${t.runtimeType}'); // t 的类型是: Test

(四)实例变量

所有实例变量都生成一个隐式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 Point {
  num x = 10;
  num y = 5;
  Point p = new Point();//p在构造函数之前执行
  Point(){}
}
void main() {
    var point = Point();
    point.x = 4; // 
}

(五)类(静态)变量和方法

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

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

void main() {
  assert(Test. num == 16);
}

静态方法(类方法)不对实例进行操作,因此无权访问this。

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

注意:对于常用或广泛使用的实用程序和功能,请考虑使用顶级函数而不是静态方法。

您可以使用静态方法作为编译时常量。例如,您可以将静态方法作为参数传递给常量构造函数。

(六)构造函数

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

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

例如:

class Test {
  num x, y;

  Test(num x, num y) {
    this.x = x;//this.x指向的是当前Test类里面的变量num x
    this.y = y;
  }
}
(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)构造函数不是继承的

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

(5)命名构造函数

Dart不像java可以使用多个同名不同参数构造。但是Dart提供了命名构造。使用命名构造函数为类实现多个构造函数或提供更多的解释说明。
例如:

class Test{
  num x, y;

  // 命名构造
  Test.help() {
    x = 5;
    y = 10;
    print('x=${x}, y = ${y}');
  }
}

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

void main() {
  // 先执行TestChild类的空参构造, 再执行Test类的空参构造。
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}');
  // }
}
(6)构造函数调用流程

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

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 有参构造');
  }
}

运行结果为:

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

注意事项:

    1. 在调用构造函数之前会计算超类构造函数的参数,所以参数可以是一个表达式。
class NewTest{
  static String data;
  NewTest() {
    print('NewTest 空参构造,data: $data');
  }

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

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

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

void main(){
new NewTestChild();
}

执行结果为:

NewTest.area2 命名函数,data: NewTestChild的数据 getDefaultData
NewTestChild 空参函数 调用父类的命名构造
  • 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)常量构造函数

如果您的类生成永远不会更改的对象,则可以使这些对象成为编译时常量。为此,请定义const构造函数并确保所有实例变量都是final。
示例代码:

void main() {
  var result = new Test(4, 10);
  print('x:${result.x}, y: ${result.y}');
}

class Test {
  static final Test origin = const Test(0, 0);
  final num x;
final num y;
  const Test(this.x, this.y);
}
(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())

(七)初始化列表

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

例如:

有参构造的初始化:

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

命名构造的初始化:

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

例如:

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

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

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

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

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

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

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

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

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

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

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

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

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

(八)方法

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

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

Dart函数的使用示例代码请看dart_demo/test/method/method_write.dart。
例如上一章中flutter_demo工程里面的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_demo工程里面的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函数的使用示例代码请看dart_demo/test/method/method_choosable.dart。
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)函数作为一个参数传给另一个函数

Dart函数的使用示例代码请看dart_demo/test/method/method_params.dart。
这个就类似于java里面的回调功能。例如上一章创建的flutter_demo项目里的main.dart,我们看看这段代码就知道了:

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

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

Dart函数的使用示例代码请看dart_demo/test/method/method_noname.dart。
我们还是以上一章创建的flutter_demo项目里的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函数的使用示例代码请看dart_demo/test/method/method_resolution.dart。

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)闭包

Dart函数的使用示例代码请看dart_demo/test/method/method_closure.dart。

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

  • 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)等价函数

Dart函数的使用示例代码请看dart_demo/test/method/method_object.dart。

//函数也是对象
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函数的使用示例代码请看dart_demo/test/method/method_alias.dart。
可以使用typedef给函数取个别名,这一点让我想起了C语言里面的函数指针。我们看看typedef的用法,举例如下:

void main(){
  // print(sort is Compare); // 返回True

  test(){
    print('这是一个函数别名的使用Demo');
  }
    // 3. main函数里面使用Demo类,给mFunction参数传入一个函数
  new Demo(
    // 这样没有输出
    // mFunction:(){
    //   print('这是一个函数别名的使用Demo');
    // },
    // 先写一个函数,然后再引用进来就可以了
    mFunction: test(),
  );
}
// 1. 给Function取一个别名叫做VoidCallback
typedef VoidCallback = void Function();
// 2. Demo类里的构造方法使用这个别名,关于类的用法后面会讲。
class Demo {
  final VoidCallback mFunction;
  const Demo({this.mFunction, String name});
}
(9)getter 和 setter

getter和setter是提供对象属性的读写访问权限的特殊方法。所有实例变量都生成一个隐式getter方法。非final实例变量也会生成隐式setter方法。使用get和set关键字通过实现getter和setter来创建其他属性。
使用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,将其值保存在临时变量中。

(10)抽象方法

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

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

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

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

(九)抽象类

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

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

(十)隐式接口

每个类都隐式定义一个接口,该接口包含该类的所有实例成员及其实现的任何接口。如果要在不继承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 Person, Life {...}

(十一)继承一个类

(1)使用extends 创建一个子类,使用super指向超类。
  class Test {
    void test() {
    // ···
    }
  // ···
}

class TestChild extends Test {
    void test() {
      super.test();
      // ···
    }
    // ···
}
(2)子类可以重写实例方法,getter和setter。您可以使用@override注解来声明您想要重写的成员:
class TestChild extends Test {
  @override
    void test() {
      super.test();
      // ···
    }
    // ···
}

要在类型安全的代码中缩小方法参数或实例变量的类型,可以使用covariant关键字。

(3)重写操作符

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

运算符 ---- ---- ----
< + | []
> / ^ []=
<= ~/ & ~
>= * << ==
- % >>

注意:!=不是可重写的运算符。表达式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);
  }
}
(4)noSuchMethod()

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

class A {
  // 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}');
  }
}

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

(十二)枚举类型

1.使用enum关键字声明枚举类型:
例如:enum Color { red, green, blue }

  1. 枚举中的每个值都有一个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).你不能显式实例化枚举。

(十三)向类添加功能:mixin (重难点,需要掌握)

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

1.要使用 mixin,请使用with关键字后跟一个或多个mixin名称。以下示例显示了两个使用mixins的类。
class Musician extends Performer with Musical {
  // ···
}

class Maestro extends Person
    with Musical, Aggressive, Demented {
  Maestro(String maestroName) {
    name = maestroName;
    canConduct = true;
  }
}
2.要实现 mixin,请创建一个扩展Object的类,并且不声明构造函数。除非您希望mixin可用作常规类,否则请使用mixin关键字而不是class。例如:
  mixin Musical {
    bool canPlayPiano = false;
    bool canCompose = false;
    bool canConduct = false;

  void entertainMe() {
      if (canPlayPiano) {
        print('Playing piano');
      } else if (canConduct) {
        print('Waving hands');
      } else {
        print('Humming to self');
      }
    }
}
3.要指定只有某些类型可以使用mixin。例如,所以你的mixin可以调用它没有定义的方法, 用于on指定所需的超类。
mixin MusicalPerformer on Musician {
//... 
}

(十四)可调用的类(Callable Class)

要允许像函数一样调用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}';
}
Flutter&Dart
Web note ad 1