Dart语言学习

Chris Scoot
14 min readApr 29, 2024

本人对C、C++、Objective- C、swift语言较为熟悉,因而学习过程中,难免将Dart语言与之进行对比,总体而言,Dart语言跟它们也有一定相似性,但是更多的借鉴了它们的一些优点。下面将主要介绍Dart与其他语言的区别

1、变量定义方式

在C、C++定义变量一般是使用类型名 + 变量名的方式,当然C++中增加auto可以使用自动类型推断,而很多现代编程语言例如Swift一般都可通过let、var等关键字实现自动类型推断,而Dart同时支持这两种方式。

  • C、C++中定义变量
int a = 9; ///C、C++、Objective-C 定义变量:类型名int + 变量名a
bool isOk = true;
auto b = a; ///C++方式,自动类型推断b的类型为int
  • swift语言定义变量
var a = 90;  ///变量a, 类型推断为int
let b = 34; ///常量b, 类型推断为int
var ab:Int32; //变量ab,指定类型为int32
Bool ccc; ///Error, 不接受这种方式
  • Dart定义变量
int iiii = 0; ///传统方式,OK
var name = 'Bob';///自动推断name变量类型为String
const cttt = 90; ///编译时常量,必须使用编译期常量初始化
final ff = "xxx"; ///运行时常量,只能初始化一次,如果没初始化就使用会报错

2、工厂构造方法 — factory

class A {
A();
factory A.name() {///本质就是类方法,与普通类方法不同的是,强制了返回值必须是A类型
return A();
}

static A name1() {///普通类方法,返回值可以随意
return A();
}
}

与Objective-C中的instancetype类似:

interface A : NSObject
+ (instancetype)name; ///类方法,必须返回A类型对象
+ (id)MethodB;///普通类方法,返回任意对象都可以
@end

3、强类型

  • 强类型,变量被定义后,其类型完全确定,后面无法将其他类型值赋值给该变量。对C、C++而言,除非两个类型可以隐式或显示类型转换,否则无法编译。
  • 弱类型,变量被定义后,可以将其他类型赋予该变量。编译器不会报错,例如JS、Objective-C,实际运行时,如果对象真实类型没有对应方法或属性则运行时出错。

强类型可以让编译器帮我们进行类型检查,减少出现运行时错误几率,而弱类型则相对较为灵活,但一不小心容易出现RunTime异常。Dart语言采用的是强类型,相对而言更不容易出现错误。

4、变量使用

C、C++、OC中的变量未初始化可以使用,但是读取的值可能是随机的,Dart语言为了安全性,编译期和运行时都会检查变量是否有初始化,如果读取一个未初始化的变量会产生异常。

  • 非空变量,必须在使用之前赋值【编译期自动检查 — 读取之前必然已经被初始化了,否则编译器提示错误】
void fun(bool isEmpty) {
int lineCount;//不一定在声明变量时初始化,只需在第一次用到这个变量前初始化即可

///下面的if/else两个分支,必须都对lineCount进行初始化,否则编译器会提示错误
if (isEmpty) {
lineCount = 0;
} else {
lineCount = 0;
}
print(lineCount);
}
  • late 申明的变量,必须为非空变量。可延时初始化,第一次读之前还未初始化则运行奔溃
    注意【编译器无法保证,需要开发人员自己保证读取前变量已初始化,否则抛异常】
void fun(bool isEmpty) {
late int lineCount;///late类型变量,必须是非空,
late String result = _getResult(); ////如果result未被使用,则_getResult则永远不执行
///下面的if/else两个分支,有一个对lineCount进行初始化即可。都不初始化则编译器会报错
if (isEmpty) {///如果运行时,执行到这则奔溃,因为lineCount没有初始化
// lineCount = 0;///OK,
} else {
lineCount = 0;
}
print(lineCount);
}
  • 可空类型变量,如果没有初始化,默认值为null
int? aaa; ///没有初始化,默认值为null
print(aaa); ///输出:null
  • final类型变量不一定要在声明时初始化,只需保证使用前必须赋值一次,而且只能赋值一次,可以用变量或常量初始化
final bool isOk;
isOk = false;
// isOk = true;///The final variable 'isOk' can only be set once.
print(isOk);
  • const变量,必须在声明时进行初始化,且必须用常量进行初始化
int a = 90;
const xxxx = a; ///Error,不能用变量进行初始化
const yyyy = 90;///Ok

5、函数参数的传递方式

  • 【值传递】对于String、Bool、int、double、Null等基础数据类型,这些类型的对象是不可修改的,一旦创建一个对象,该对象无法被修改。因而会拷贝副本,二者之间互不影响。
  • 【引用传递】其他对象(自定义的class、Map、Set、List等)拷贝的是对象地址,由于二者指向同一个对象,因而通过任一变量修改该对象的属性或状态,都会影响其他变量。

注意:【这里的引用传递与C++中的引用传递意义不同,更像是值传递,拷贝地址的一个副本,所以官方说法是都是值传递,理解即可】参考代码:

void func11(int num, List<int> array, List<int> a2) {
num = 100; ///修改的是副本,不影响外部变量
array.add(34); ///通过地址修改改对象,会影响外部

a2 = [9, 8];///修改地址,指向另一个数组,不影响外部

print("func11");
print(num);
print(array);
print(a2);
}

void main() {
int i = 1;
var array = [1, 6];
var array22 = [4, 5];

func11(i, array, array22);///i 拷贝一份,array、array22拷贝了地址

print("-----");
print(i); ///1
print(array);/// [1, 6, 34]
print(array22); //[4, 5]
}

输出结果如下;
flutter: func11
flutter: 100
flutter: [1, 6, 34]
flutter: [9, 8]
flutter: -----
flutter: 1
flutter: [1, 6, 34]
flutter: [4, 5]

另一个例子:

 int old_I = 90;
var new_I = old_I; ///把90拷贝到新的变量中
old_I++;
print("old---$old_I---new---$new_I");

var old_Str = "uuu";
var new_Str = old_Str;///把字符串拷贝到新的变量中
old_Str = "iiii";
print("old---$old_Str---new---$new_Str");

{
var old_list = [4, 5, 9];
var new_list = old_list;///把数组对象的地址拷贝到新的变量中,二者指向的是相同的数组对象
old_list = [78, 90];///把新的数组对象的地址赋值到该变量
print("old---$old_list---new---$new_list");
}

var old_list = [4, 5, 9];
var new_list = old_list;///把数组对象的地址拷贝到新的变量中,二者指向的是相同的数组对象
old_list.clear();///把对象中元素情况,由于二者指向的是同一个数组对象,所以二个数组都为空
print("old---$old_list---new---$new_list");

输出结果如下:
flutter: old---91---new---90
flutter: old---iiii---new---uuu
flutter: old---[78, 90]---new---[4, 5, 9]
flutter: old---[]---new---[]

6、级联运算符(.., ?..)

这个运算符是Dart语言特有的,可以让你在同一个对象上连续调用多个对象的变量或方法。比如以下代码:

var paint = Paint() 
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;

以上代码完全等同于如下代码:

var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

如果该对象可能为空的话,那么第一次使用级联运算符时应该使用 ?.. 以保证后续操作不会发生在null对象上。比如以下代码:

querySelector('#confirm') //返回的对象可能为空
?..text = 'Confirm' //第一次必须使用 ?.. 如果上一句为空,后续操作不会进行
..classes.add('important') //后续的用不用?号无所谓,因为如果为空,执行完上一句就返回了,后续都不会执行
..onClick.listen((e) => window.alert('Confirmed!'))
..scrollIntoView();

这段代码,等同于以下代码:

var button = querySelector('#confirm');
button?.text = 'Confirm';
button?.classes.add('important');
button?.onClick.listen((e) => window.alert('Confirmed!'));
button?.scrollIntoView();

级联运算符可以嵌套,例如:

final addressBook = (AddressBookBuilder()
..name = 'jenny'
..email = 'jenny@example.com'
..phone = (PhoneNumberBuilder()
..number = '415-555-0100'
..label = 'home')
.build())
.build();

7、一切皆对象

所有类型都是Object的子类型。这点跟OC类似,值得注意的是,以下是Object和dynamic的异同:

  • 【相同点】都可以指向任意类型的对象
  • 【不同点】Object对象只能调用Object类的方法和属性,否则无法通过编译,而dynamic则相反,调用任意方法都能通过编译【即使不存在】,运行时若不存在该方法则抛出异常
 void build() {
const str = "text";///String类型
str.length; ///OK

Object obj = "aaa";///可以指向任意对象,这里实际是String类型
///下面代码无法编译,因为Object类没有length方法,
obj.length;///The getter 'length' isn't defined for the type 'Object'.


dynamic dy = 90;///可以指向任意对象,实际是int类型
dy.abc(); ///Error:int没有abc方法,但是编译期间不报错,运行时崩溃,找不到此方法
}

8、闭包对外部局部变量的捕获方式

低级语言C++、Objective-C中的闭包(lamda表达式或Block)对外部局部变量的捕获分为拷贝捕获和引用捕获,但是大部分高级语言一般都只保留引用捕获【因为语言本身会自动管理内存】。

引用捕获特点:让开发人员觉得不管是在在闭包中还是闭包外部使用函数的局部变量,都是安全的,开发人员无须担心局部变量会在函数返回后会被释放,一切交给编译器管理。

  • C++的lamda表达式中,=为拷贝捕获,&为引用捕获。所以开发人员需要非常小心,一旦lamda中引用捕获了函数局部变量,务必保证lamda的调用必须在函数返回之前,否则会崩。
  • Objective-C中普通局部变量也是拷贝捕获,__block关键字则导致代码中所有对该局部变量的访问,偷偷修改成访问堆中的变量(偷梁换柱),所以即便函数返回后再执行block也是安全的,如果是C++的引用则可能引发奔溃(因为局部变量在函数返回后已释放,但是lamda可能在函数返回后再调用)
void test(){
__block int aaa = 0;
NSLog(@"----a:%p",&aaa); ///在栈上
dispatch_async(dispatch_get_main_queue(), ^{
aaa = 1;
});
NSLog(@"+++a:%p",&aaa);///在堆上,现在的aaa已经不是原来的aaa了
}

//输入如下:
2023-07-19 23:01:33.042980+0800 test[19863:3236537] ----a:0x16b4bd248
2023-07-19 23:01:33.043081+0800 test[19863:3236537] +++a:0x60000237d098
  • Dart、Swift、Go等现代高级语言中对局部变量只有引用捕获,所以效果看起来都是OC中的__block修饰后的效果,参考代码:
void test() {
var i = 20;///局部变量Int
var array = [8, 9, 0];///局部变量数组

//1秒后这个i行,此时函数已返回,局部变量已经不存在了按道理
Future.delayed(Duration(milliseconds: 1000), () {//捕获了外部局部变量i和array
print("delayed----->");
print(i);
print(array);

i = 90; ///
array.add(100);
}).then((value) {//捕获了外部局部变量i和array
print("then----->");
print(i);
print(array);
});

i = 12;
array.add(888);

print("test----->");
print(i);
print(array);
}

void main() {
test();
print("--end--");
}

上述代码的输入结果如下:【闭包没有对捕获变量copy】

flutter: test----->
flutter: 12
flutter: [8, 9, 0, 888]
flutter: --end--
flutter: delayed----->
flutter: 12
flutter: [8, 9, 0, 888]
flutter: then----->
flutter: 90
flutter: [8, 9, 0, 888, 100]

9、单继承、Mixin(支持implement实现若干接口)

除了C++支持多继承外,大部分现代语言为了避免菱形继承问题,都只支持单继承。Swift、OC、Dart、Java都只支持单继承,值得注意的是dart中通过Mixin类,可以间接的以单继承的方式实现多继承功能。

关于Mixin,是dart中一个特有的玩法,很有意思,下次将详细介绍其技巧和特点。

点击参考Dart 语言中extends、implements、mixin的区别

10、Dart VM自动内存管理(程序员不用关心对象释放问题)

同时支持JIT(Just-In Time)、AOT(ahead of Time)

  • Just-In Time 即时编译,在程序运行时,一边解释,一边执行。执行效率低,但是移植性强。
  • Ahead of Time 提前编译,也就是在程序运行前,提前编译成当前CPU支持的机器码,执行效率高,但无法将编译好的程序移植到其他CPU体系上。

Dart支持开发调试时使用JIT模式,而发布时使用AOT模式,鱼与熊掌兼得。

11、函数形参定义的多样化

12、Future异步处理

13、Stream流使用

--

--

Chris Scoot
0 Followers

擅长C语言、C++、iOS开发(Objective-C\Swift)、Flutter开发(Dart语言)、GO语言、Python等,拥抱新技术,热爱AI领域,对OpenCV、Pytorch\Tensorflow充满热情。