由于是面向对象语言,所以Dart同样也有class类

因为我们对类已经相当熟悉了,这里就用代码描述一下吧

类中包含对象的属性和方法(也就是定义的行为)

和C#一样,定义了类只有实例化后才能使用类(对了,dart)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void main(List<String> args) {
//类的实例,也可以写成Player p =new Player();
//或者var p = new Player();
//在Dart中,new关键字是可选的,可以省略不写。
var p = Player();
p.name = "勇者";
p.hp = 100;

p.attack();
}

class Player {
String? name;
int? hp;

void attack() {
print('$name 发动攻击');
}
}

同样的,有类必定有构造函数

构造函数

构造函数就是一个特殊的方法,用于创建对象并初始化对象的属性。它的名称与类名相同,并且没有返回类型。在 Dart 中,构造函数可以有参数,也可以没有参数。

说人话就是我们在初始化类时,dart会自动加载构造函数中的参数设置

不过和C#还有Java不同的是这个Dart构造函数的重载

默认构造

这里我们先说无参的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void main(List<String> args) {
Player player = Player();
print('玩家名字:${player.name}, 玩家血量:${player.hp}');
}

class Player {
String? name = '';
int? hp = 0;

void attack() {
print('$name 发动攻击');
}

//无参构造函数
Player() {
name = '善良米塔';
hp = 100;
}
}

然后是有参构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void main(List<String> args) {
Player player = Player('帽子米塔', 100);
print('玩家名字:${player.name}, 玩家血量:${player.hp}');
}

class Player {
String? name = '';
int? hp = 0;

void attack() {
print('$name 发动攻击');
}

//有参构造函数
Player(String? name, int? hp) {
this.name = name;
this.hp = hp;
}
}

不过以上的写法比较繁琐,因为还得考虑参数为空的情况,下面我们就用简洁的方式,不用?符号

无参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main() {
var player = Player();
print('玩家名字:${player.name}, 玩家血量:${player.hp}');
}

class Player {
String name;
int hp;

// 无参构造(推荐写法)
Player() : name = '善良米塔', hp = 100;

void attack() {
print('$name 发动攻击');
}
}

有参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main(List<String> args) {
Player player = Player('帽子米塔', 100);
print('玩家名字:${player.name}, 玩家血量:${player.hp}');
}

class Player {
String name;
int hp;

Player(this.name, this.hp);//语法糖,我们后面会说

void attack() {
print('$name 发动攻击');
}
}

命名构造(Dart 特有,很常用)

image-20260416143744161

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void main(List<String> args) {
Player player = Player.NPC('帽子米塔', 100);
print('玩家名字:${player.name}, 玩家血量:${player.hp}');
}

class Player {
String? name = '';
int? hp = 0;

void attack() {
print('$name 发动攻击');
}

//有参构造函数
Player.NPC(String? name, int? hp) {
this.name = name;
this.hp = hp;
}
}

然而这种写法仍然繁琐,看的鸡肋

还有三种直观的写法

①兼顾Player和Player.NPC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void main(List<String> args) {
Player npc = Player.NPC('帽子米塔', 100);
Player player = Player('玩家1', 100);
print('玩家名字:${player.name}, 玩家血量:${player.hp}');
print('NPC名字:${npc.name}, NPC血量:${npc.hp}');
}

class Player {
String name;
int hp;

// 主构造
Player(this.name, this.hp);

// 命名构造
Player.NPC(String name, int hp) : this(name, hp);

void attack() {
print('$name 发动攻击');
}
}

②语法糖写法,这个下面会讲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void main(List<String> args) {
Player npc = Player.NPC('帽子米塔', 100);
Player player = Player('玩家1', 100);
print('玩家名字:${player.name}, 玩家血量:${player.hp}');
print('NPC名字:${npc.name}, NPC血量:${npc.hp}');
}

class Player {
String name;
int hp;

Player(this.name, this.hp);
Player.NPC(this.name, this.hp);

void attack() {
print('$name 发动攻击');
}
}

③使用命名参数(不初始化值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void main(List<String> args) {
Player npc = Player.NPC(name: '帽子米塔', hp: 100);
Player player = Player(name: '玩家1', hp: 100);
print('玩家名字:${player.name}, 玩家血量:${player.hp}');
print('NPC名字:${npc.name}, NPC血量:${npc.hp}');
}

class Player {
String name;
int hp;

Player({required this.name, required this.hp});
Player.NPC({required this.name, required this.hp});

void attack() {
print('$name 发动攻击');
}
}

④使用命名参数(初始化值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void main(List<String> args) {
Player npc = Player.NPC();
Player player = Player();
print('玩家名字:${player.name}, 玩家血量:${player.hp}');
print('NPC名字:${npc.name}, NPC血量:${npc.hp}');
}

class Player {
String name;
int hp;

Player({this.name = "玩家1", this.hp = 100});
Player.NPC({this.name = "NPC1", this.hp = 100});

void attack() {
print('$name 发动攻击');
}
}

语法糖

image-20260416154014857

这个上面已经演示过了,格式就是这样,用来简化代码

公有属性和私有属性

学过C#和java还有JavaScript的都知道public和private这两个东西,通常来说在这些语言中是默认属性和函数私有的,只有显式声明public才会变成公有

不过,在Dart里,默认的属性是共有的,(这点和lua很像,lua也是只有标注locla关键字才是私有)

在Dart中私有属性以下划线开头,如

我们创建三个文件

1
2
3
4
5
flutter学习
├── 1111
│ └── closed.dart
├── start.dart
└── test.dart

closed.dart

1
2
3
4
5
6
class Enemy {
final String name;
int hp;

Enemy(this.name, this.hp);
}

start.dart

1
2
3
4
5
6
7
8
9
import 'test.dart';
import '1111/closed.dart';

void main() {
var p = Player('勇者', '男', 100);
var e = Enemy('哥布林', 50);
print('玩家:${p.name},性别:${p._gender},HP:${p.hp}');//这里会报错,因为_player.dart中的_gender是私有属性,无法在main.dart中访问
print('敌人:${e.name},HP:${e.hp}');
}

test.dart

1
2
3
4
5
6
7
class Player {
final String name;
final String _gender;
int hp;

Player(this.name, this._gender, this.hp);
}

这里看到报错了,因为我们把gender设置为私有属性,别的文件无法访问

image-20260416173331577

导入的话就是用import来导入相对路径的文件,例如

1
2
3
import 'test.dart';
import '1111/closed.dart';
import'./1111/closed.dart';

这里我们还需要了解一个东西就是import的起别名和导入控制

起别名

这个主要是应对当不同文件存在同名类的情况

例如我们创建三个文件\

player.dart

1
2
3
class Player {
void info() => print('Player');
}

enemy.dart

1
2
3
class Player {
void info() => print('Player');
}

可以发现这两个类都叫做player,然而我们导入时编译器分不清

所以可以分别取别名使用as关键字,然后我们实例化时也要在类前面加上别名的前缀

1
2
3
4
5
6
7
8
9
10
import 'player.dart' as player;
import 'enemy.dart' as enemy;

void main() {
var p1 = player.Player();
var p2 = enemy.Player();

p1.info();
p2.info();
}

导入控制

例如我们将上述的enemy.dart脚本改为

1
2
3
4
5
6
7
8
9
class Player {
void info() => print('Enemy Player');
}

class A {}

class B {}

class C {}

但是我们只想用Player类就必须这么写使用show关键字就可以导入单独的类

1
2
3
4
5
6
7
8
9
10
import 'player.dart' as player show Player;
import 'enemy.dart' as enemy;

void main() {
var p1 = player.Player();
var p2 = enemy.Player();

p1.info();
p2.info();
}

但是假如我们不想用某个类但是其他的仍需要就可以使用hide关键字

这里我们可以修改一下enemy脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Player {
void info() => print('Enemy Player');
}

class A {
A() {
print("我要吃提瓦特煎蛋");
}
}

class B {
B() {
print("我要吃蒙德烤鱼");
}
}

class C {
C() {
print("我要吃渔人吐司");
}
}

然后我们修改我们的game脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'player.dart' as player show Player;
import 'enemy.dart' as enemy;

void main() {
var p1 = player.Player();
var p2 = enemy.Player();

p1.info();
p2.info();

var a = enemy.A();
var b = enemy.B();
var c = enemy.C();

print('$a, $b, $c');
}

此时我们将enemy的导入修改为

1
import 'enemy.dart' as enemy hide A;

继承

继承是面向对象的三大特点:继承,多态,封装之一,就是子类会获得父类的所有属性和方法,封装这个最好理解,比如函数本身就是封装,你在使用函数的时候无需在意函数内部怎么实现的,只需要学会怎么使用这个函数提供的功能即可

image-20260416191201330

extends关键字

Dart 是单继承语言,也就是一个类只能继承一个父类

和C#差不多,不过和Java更像,C#中使用的是“:”来继承父类或者接口

在Dart中使用的是extends

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal {
void eat() {
print("吃东西");
}
}

class Dog extends Animal {
void bark() {
print("汪汪");
}
}

void main() {
var d = Dog();
d.eat(); // 继承来的
d.bark(); // 自己的
}

super关键字构造函数继承

虽然咱们说了子类可以继承父类的所有属性,但是除了构造函数不能,所以需要一个特殊的关键字super

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal {
String name;

Animal(this.name);
}

class Dog extends Animal {
Dog(String name) : super(name);
}

void main() {
var d = Dog("旺财");
print(d.name);
}

初次之外,super还能调用父类中的方法,来接受被子类覆盖掉的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Animal {
String name;

Animal(this.name);

String stickOutTongue() {
return "$name 吐舌头";
}
}

class Dog extends Animal {
Dog(String name) : super(name);

@override
String stickOutTongue() {
print(super.stickOutTongue()); // 调用父类的方法,保留父类的功能
return "$name 伸出舌头";
}
}

void main() {
var d = Dog("旺财");
var a = Animal("动物");
print(d.stickOutTongue());
print(a.stickOutTongue());
}

多态

多态就是同一操作作用于不同对象可以产生不同的结果

override关键字方法重写

这个和C#很像,因为C#里也有override关键字可以让子类修改继承自父类的属性,语法是 @override

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
void sound() {
print("动物发声");
}
}

class Dog extends Animal {
@override
void sound() {
print("汪汪");
}
}

abstract关键字抽象类

注意抽象类不能直接实例化,这个和C#中的abstract关键字是一样的

特点

  • 不能 new Animal()即new实例化

  • 可以有:

    • 抽象方法(必须子类实现)

    • 普通方法(子类可以直接用)

    • 子类必须实现所有抽象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
abstract class Animal {
void sound(); // 没有实现
}

class Dog extends Animal {
@override
void sound() {
print("汪汪");
}
}

class Cat extends Animal {
@override
void sound() {
print("喵喵");
}
}

void main(List<String> args) {
Animal dog = Dog();
Animal cat = Cat();
dog.sound();
cat.sound();
}

implements关键字继承接口

这个不会继承父类中的实现(即只是把一个类当“接口”用),只会继承属性和方法,但是不会继承属性值和方法里的内容,需要override重写,对了,使用这个可以实现抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
abstract class Animal {
void sound(); // 没有实现
}

class Dog implements Animal {
@override
void sound() {
print("汪汪汪");
}
}

class Cat implements Animal {
@override
void sound() {
print("喵喵喵");
}
}

void main(List<String> args) {
Animal dog = Dog();
Animal cat = Cat();
dog.sound();
cat.sound();
}

对了还有一个叫做mixin混入,不过我们接下来会说,这个东西可以实现多继承

mixin类的混入

mixin类似godot中的组合的方式,比如把不同功能独立开来,然后需要的时候启用这个模块就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
abstract class Animal {
void sound();
}

//mixin实现功能的复用,不需要继承关系,将功能进行分离,需要什么功能就混入什么功能
mixin Eat {
void eat() => print("吃");
}

mixin Fly {
void fly() => print("飞");
}

mixin Swim {
void swim() => print("游");
}

mixin Run {
void run() => print("跑");
}

// 继承Animal,同时混入不同的功能
class Dog extends Animal with Run, Eat {
@override
void sound() => print("汪汪");
}

class Chicken extends Animal with Swim, Eat {
@override
void sound() => print("咯咯");
}

class Duck extends Animal with Fly, Swim, Run, Eat {
@override
void sound() => print("嘎嘎");
}

void main() {
var dog = Dog();
dog.sound();
dog.run();
dog.eat();

var chicken = Chicken();
chicken.sound();
chicken.swim();
chicken.eat();

var duck = Duck();
duck.sound();
duck.fly();
duck.swim();
duck.run();
duck.eat();
}

这东西我个人觉得非常好用,至少比继承好用

不过还存在一个问题,如果不同的mixin里面存在相同名字的方法,就会发生覆盖的现象,后添加的会覆盖先添加的

image-20260416204322018

例如我们如果把A,B都混合进C类,后添加的B的同名方法和属性会覆盖A的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mixin A {
void say() {
print("A");
}
}

mixin B {
void say() {
print("B");
}
}

class C with A, B {}

void main() {
C().say(); //B
}

所以我们尽量避免不同的mixin实现相同的功能,最好也一个mixin实现单个功能,这样既可以减少代码量,也可以避免覆盖

泛型

这个东西我们在学List时其实已经见过了

1
2
List<int> numbers = [1, 2, 3];
List<String> names = ["a", "b"];

我们都知道List和C#中的List几乎一样,只不过C#强制List必须有泛型,而我们的Dart在没有加泛型的时候会默认为List< dynamic >类型

我们在实际使用的时候应该加上泛型进行参数类型限制

这里我们可以根据我们已经学过的泛型集合和泛型方法还有泛型类来讲

泛型集合(Map和List)

List我们就不说了,就说说Map吧

1
2
3
4
5
6
7
8
9
10
11
void main(List<String> args) {
//泛型字典
Map<String, String> characterMap = {
"芙卡洛斯": "水神",
"那维莱特": "龙王",
"娜维娅": "刺玫会长",
};
characterMap.forEach((key, value) {
print("$key: $value");
});
}

泛型方法(Function)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void main(List<String> args) {
List<String> player = ['芙宁娜', '桑多涅', '哥伦比亚'];

print('第一个玩家是: ${getFirst(player)}');
printType(player);
}

//泛型方法
T getFirst<T>(List<T> list) {
return list[0];
}

void printType<T>(List<T> list) {
for (var item in list) {
print(item);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
T echo<T>(T value) {
return value;
}

void main() {

print(
echo<int>(123)
);

print(
echo<String>("hello")
);

}

泛型类(Class)

如果对于背包类的话,就是什么类型都能装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Box<T> {

T item;

Box(this.item);

void show() {
print(item);
}

}

void main() {

Box<int> a = Box(100);

Box<String> b = Box("芙宁娜");

a.show();

b.show();

}

当然也可以用于不同类之间

如创建animal.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
abstract class Animal<N, A> {
N name;
A age;

Animal(this.name, this.age);
}

mixin Fly {
void fly() => print('Flying');
}

mixin Eat {
void eat() => print('Eating');
}

mixin Sleep {
void sleep() => print('Sleeping');
}

创建dog.dart

1
2
3
4
5
6
7
8
9
10
11
import 'animal.dart';

void main() {
Dog<String, int> dog = Dog("Buddy", 5);
dog.eat();
dog.sleep();
}

class Dog<N, A> extends Animal<N, A> with Eat, Sleep {
Dog(N name, A age) : super(name, age); //这里用super使用父类的构造函数来初始化name和age属性
}

对于这个泛型我个人的理解是,除非你知道为什么用,否则就不要用,因为这个东西主要是为了让代码更健壮的,并不是必要的