浅谈 Dart 类与类的基本方法

4,399 阅读6分钟

一、Dart 语言

Dart 是一门面向对象的语言。

Dart 语言中所有的变量引用都是一个对象,并且所有的对象都对应一个类的实例。无论是数字、函数和null ,都是一个对象,所有对象继承自Object类。在 2.12 版本 Dart 新增空安全后(sound null safety),null不再是Object的子类。

Object

Object 具有下列特性:

  • 两个属性:hashCoderuntimeType
  • 两个方法:noSuchMethodtoString
  • 操作符:Operator

hashCode

hashCode 是 read-only 属性的 int 类型数据,因为 Dart 所有的变量引用均继承于Object,所以每个变量引用都有自己的hashCodehashCode的设计目的是为了提高 Dart VM 引擎在解析数据结构时的效率,并且参与判断对象的逻辑运算,与操作符 operator 相辅相成。考察如下代码:

void main() {
    P p1 = P("yo","no");
    P p2 = P("yo","no");
  
    print(p1 == p2);
    print(p1.hashCode);
    print(p2.hashCode);
    
    Map<P, String> myMap = {};
    myMap[p1] = "first Item";

    print(myMap.containsKey(p2));
}

class P {
    var naam;
    var kaam;
    P(this.naam, this.kaam);
    
    @override
    int get hashCode {
        int result = 11 * naam.hashCode;
        result = 13 * (result + kaam.hashCode);
        return result;
    } 
    
    @override
    bool operator == (dynamic otherObj) {
        return (otherObj is P) && (otherObj.naam == naam) && (otherObj.kaam == kaam);
    }
}

代码重写了P对象的比较相等的逻辑,从而令p1p2的比较结果返回为true。但同时需要重写hashCode的逻辑,否则返回值仍旧为false

Dart 建议hashCodeOperator如有必要重写其中之一,那么另一个也需要重写,从而令哈希映射正常工作,否则Operator重写无效。

runtimeType

runtimeType是 read-only 属性,表达数据结构在运行时的类型。runtimeType涉及 Dart VM 运行机制,此处留坑以后再来填。

noSuchMethod

当调用对象上不存在的方法时,就会触发noSuchMethod,考察如下代码:

void main() {
    P p = P();
    print(p.add(888));
}

class P {
    @override
    noSuchMethod(Invocation invocation) => 
        'Got the ${invocation.memberName} with arguments ${invocation.positionalArguments}';
}

在 DartPad 编辑器运行上述代码,发现并未触发noSuchMethod方法, 而是直接抛出了编译错误。这是因为noSuchMethod触发需要有两个条件:

  • 调用方必须是dynamic类型;
  • 调用方具有被调用方法的定义但未实现,同时noSuchMethod也被重写;

所以只有如下两种情况,才会触发noSuchMethod

void main() {
    dynamic p = P();
    Q q = Q(); // 该情况下,q 可以是 Q 类型,也可以是 dynamic
    print(p.add(123));
    print(q.add(123));
}

class P {
    @override
    noSuchMethod(Invocation invocation) => 'Got the ${invocation.memberName} with arguments ${invocation.positionalArguments}';
    }
    
class Q {
    miss(int data);
    @override
    noSuchMethod(Invocation invocation) => 'Got the ${invocation.memberName} with arguments ${invocation.positionalArguments}';
}

toString

toString方法使用字符串来表达对象的信息,也可以在将数字转换为字符串的场景下使用。开发者也可以重写 toString方法,以加入自定义内容。

void main() {
    P p = P();
    p.toString();
}

class P {
    @override
    toString() {
        return 'this is custom text'
    } 
}

二、Dart 类与基本方法

1.类的定义和构造函数

class P {
    String name;
    int age;
    
    P(String dataName, int dataAge) {
        name = dataName;
        age = dataAge;
    } 
    
    bool isOld() {
        return age > 30 ? true : false;
    } 
}

void main() {
    var p = new P('tom', 12);
    print(p.name);
}

上述代码中,声明了P类,P 中含有两个属性:nameage。同时也声明了构造函数,通过向构造函数传入参数从而创建实例p

创建实例仍然使用传统的new方法,但在 Dart 2 以上版本,new关键字可以省略,同时构造函数也可以用语法糖简化,代码写法如下:

class P {
    String name;
    int age;
    
    P(this.name, this.age);
    
    bool isOld() {
        return age > 30 ? true : false;
    }
}

void main() {
    var p = P('tom', 12);
    print(p.name); // tom
}

命名构造函数

除默认的构造函数外,Dart 提供命名构造函数方法。代码如下:

class P {
    String name;
    int age;
    
    P(this.name, this.age);
    
    P.init(String dataName, int dataAge) {
        name = dataName;
        age = dataAge;
    } 
    
    bool isOld() {
        return age > 30 ? true : false;
    }
}

void main() {
    var p = P.init('tom', 12);
    print(p.name); // tom
}

命名构造函数的功能看起来与默认构造函数的功能类似,那设计该机制的目的是什么呢?原因是 Dart 不支持构造函数的重载,无法使用不同的参数来执行构造方法,所以提供命名构造函数的机制来实现多方式创建实例。

工厂构造函数

除了上述两种构造函数外,Dart 还提供第三种构造函数:工厂构造函数。它的使用场景是:如果调用构造函数时,如果实例已存在,不会重新创建实例,而是使用已存在的实例,保证环境内只有一个实例存在,也就是单例模式。考察如下代码:

class P {
    String name;
    static Map<String, dynamic> _cache = {};
    
    factory P(String name) {
        if (_cache.containsKey(name)) {
            return _cache[name];
        } else {
            final p = P._internal(name);
            _cache[name] = p;
            return p;
        }
    }
    
    P._internal(this.name);
}

void main() {
    final p = P('tom');
    print(p.name);
}

工厂构造函数不允许访问this,所以_cache的形态必须是static。每次创建实例时,如果实例已经在_cache中存在,那么返回已存在的实例。换句话说,工厂构造函数并没有自动创建实例,而是把决定权交给开发者。

2.类的属性和方法

静态变量和静态方法

和大多数语言一样,Dart 提供静态变量和静态方法:

class P {
    static age;
    static bool isOld() {
        return age > 30 ? true : false;
    }
}

静态方法无法使用this,也不能访问非静态成员,类的实例也无法调用静态方法,并且静态变量只有在被使用的时候才会初始化。

私有属性和私有方法

Dart 不提供类似publicprotected等关键字,在变量前添加下划线即可声明私有:

class P {
    int _age;
    P(this._age);
    
    bool _isOld() {
        return this._age > 30 ? true : false; 
    } 
    
    bool isOld() { 
        return _isOld(); 
    }
}

void main() {
    final p = P(20);
    print(p._age);     // error
    print(p._isOld()); // error 
    print(p.isOld());  // false 
}

实例无法直接调用私有方法,但是可以通过调用公有方法的形式间接调用私有方法。

3.类的继承

构造函数

Dart 通过extends关键字实现继承,子类继承父类中公有的属性和方法,不会继承构造函数,所以子类的构造函数需通过super关键字来调用或改造父类的构造函数:

class P {
    num name;
    num age;
    
    P(this.name, this.age);
    P.xxx(this.name, this.age); 
}

// class Q extends P {
//     Q(num name, num age): super(name, age); 
// } 

// class Q extends P { 
//     num sex; 
//     Q(num sex, num name, num age): super.xxx(name, age) {
//         this.sex = sex;
//     } 
// } 

class Q extends P { 
    num sex; 
    Q(num sex, num name, num age): super(name, age) { 
        this.sex = sex;
    }
}

void main() {
    final q = Q(12, 13, 14);
    print(q.sex); // 12 
}

上述代码演示了子类中三种构造函数的表现:

  • 直接复用父类构造函数
  • 复用和改造父类默认构造函数
  • 复用和改造父类命名构造函数

子类调用父类方法

如果子类需要调用父类方法,同样使用super关键字,此时方法内部的this指向子类:

class P {
    num name;
    num age; 
    
    P(this.name, this.age); 
    
    bool childCheck() { 
        return age > 20 ? true : false; 
    } 
} 

class Q extends P { 
    Q(num name, num age): super(name, age); 
    
    bool check() { 
        return super.check(); 
    } 
}

void main() { 
    final a = Q(12, 13); 
    print(a.childCheck()); // false
}

子类重写父类方法

class P { 
    num name; 
    num age; 
    
    P(this.name, this.age); 
    
    bool check() { 
        return age > 20 ? true : false; 
    } 
} 

class Q extends P { 
    Q(num name, num age): super(name, age); 
    
    @override
    bool childCheck() { 
        return age == 13 ? true : false; 
    } 
}

void main() { 
    final a = Q(12, 13); 
    print(a.childCheck()); // true 
}

子类在重写父类方法时,只要方法名称与父类相同,便可实现逻辑重写,@override为可选项,对于复杂逻辑的类功能建议添加@override