Dart基础
正式版和之前的Dart教程版本的唯一不同就是适当调换了一下顺序,并且新增了一些内容
这些内容我们应该会在一节课内讲完(,建议基础较弱的同学提前开始卷如果你对其中的一些东西的原理感到疑惑,不用过多纠结,记住怎么用就行
最后再bb一句,我们是要在一节课内速通这门编程语言,你准备好了吗,前面可是地狱啊🔥😈
Dart语言基础
Flutter框架使用Dart语言开发,我们要使用Flutter自然先要学会如何使用Dart。个人感觉Dart像是Java和JS风格的结合体,它吸收了这两种编程语言的优点,它即可以写出像Java那样带有类型声明的清晰代码,也能在一些逻辑较为简单的地方像JS那样使用动态类型简化编码工作。如果你对Java或者JS比较熟悉,那么你应该能很快掌握这门语言的基本用法。
超高速速通(适合有基础的同学)
以下是Dart的大致语法规则,有编程经验的同学看了过后能很快上手使用,看不懂也没关系,后面会详细介绍
- 入口函数是
void main(){}
- Dart的代码块和Java和JS相同,都是使用
{}
来标识一段代码块,例如方法定义,类的定义,循环等,并且支持C风格的循环和if判断 - 每条语句都要以分号结尾
- 类似C/C++,Dart语言也有一个入口函数
main
,程序从这个函数开始执行。 - 列表使用
[]
包裹,Map使用{}
包裹,字符串使用单引号或双引号包裹(两者等效) - 单行注释使用
// 注释内容
表示,多行注释使用/* 注释内容 */
表示 if
while
for
等语句使用类似(){ // 代码体 }
的语法- 比较变量使用
<=
>=
==
!=
等运算符 - 逻辑与使用
&&
,逻辑或使用||
,逻辑非使用!
- 支持使用加号进行字符串拼接,支持模板字符串,写法为:
"${yourString}some const string"
- 函数定义的格式(这里只展示最基本的格式,后续有更复杂的函数定义):<返回值的类型> <函数名>(<参数列表>){ <函数体> }
- 变量声明可以使用
变量类型 变量名=初始化值
的风格,当然,Dart还有自己的变量声明风格。
基本语法
入口函数
同Java和C/C++,Dart语言也有一个入口函数,所有Dart程序都从这个入口函数开始执行,包括我们后面写的Flutter代码。这个入口函数就是main
:
1 | void main(){ |
输出语句
我们可以使用print()
函数来输出我们想要输出的内容:
1 | void main(){ |
print()
有一个缺点,当输出内容过长时,它有一个最大长度限制,超出限制的部分将会被直接丢弃。我们在实际开发中经常会遇到这种情况,比如当我们需要查看后端返回来的json数据时,最直接的方式就是将其打印到控制台,这个数据若是过长,那么将无法看到它的所有内容!
这时我们可以使用debugPrint()
函数,这个函数的用法和print()
一模一样,但它不会截断任何部分,而是原样输出。我们在Flutter的开发中也多用这个函数,而不是print()
。
关于Dart的输入语句,这里不专门介绍,因为我们后续使用Flutter开发的时候,输入基本上都是来自于APP内部的文本框,大可不必使用Dart的标准输入流
关于标准输入/输出流:这是所有编程语言通用的概念(除了JavaScript),标准输入流指的就是键盘;标准输出流指的是屏幕,多为控制台,例如Windows的CMD窗口
数据类型
和其他编程语言一样,Dart也有数据类型这个概念,最基本的,有:int, double, String, bool
1 | int a = 10; // int整数类型 |
除了上述基本类型外,Dart还有许多内置的高级类型,例如Socket套接字类型,Dart还能定义自定义类型(类),我们后面会讲到。
字符串有关的运算
首先是字符串的拼接,我们可以使用加号来将两个字符串拼接到一起:
1 | String str1 = "学线, "; |
然后是字符串和数字相乘:
1 | print("学线" * 2); // 输出: 学线学线 |
字符串只能和非负整数相乘(乘0会变成空字符串""
,注意不是null
),表示重复当前字符串多少次。
下面这个语法比较重要,叫模板字符串:
1 | String str1 = "移动"; |
所谓模板字符串,就是在一个字符串里面嵌入一些变量,这些变量使用${}
包起来,Dart就能自动提取变量里面的值,替换到字符串里面
思考:如何用模板字符串来改写字符串的拼接呢?
达成成就:int, double, String, bool 一家人整整齐齐
数组类型(List)
数组就是存放一系列元素的容器。接下来介绍数组的基本语法,创建数组:
1 | List lis = []; // 创建一个空数组, 常用 |
上面的的代码中,涉及到了泛型的知识,我们会在后续介绍
使用数组中的数据的时候我们使用“索引法”,每个数组元素都有一个属于自己的索引,按照它自己在数组中的顺序,从0开始,具体的方式是先写上数组名,然后在其后加一个中括号,中括号中写上想要取出来的元素的索引,例如:
1 | List lis = ["ok","mobile" , "Hello"]; |
同样的,使用索引法可以修改数组中的元素:
1 | List lis = ["ok", "mobile", "Hello"]; |
添加元素可以使用数组的add
和insert
方法,他们的区别在于,add
方法是在数组的末尾添加元素,insert
方法可以在数组的任何位置添加元素, 示例如下:
1 | List lis = ["ok", "mobile", "Hello"]; |
add
方法好理解,直接在里面写上你想要添加的元素即可,insert
方法的用法是insert(新元素的索引, 新元素)
,使用insert
方法添加元素会让原来在此索引的元素及其后面的所有元素都向后移动一位,很合理吧,不然怎么给新元素腾出空间呢(,其实就和你排队时被插队了一个道理((
注意,如果这个列表被限制了能存入的数据的类型(使用泛型),那么尝试修改或添加不合法的数据类型时,会报错。
删除数组中的元素使用remove
和removeAt
方法,其中,remove方法接受一个元素作为参数,它会从数组的第一个元素开始找,直到遇到与传入的元素相同的第一个元素,然后将其删除,这个方法的返回值是一个布尔值,如果删除成功就会返回true,否则返回false,比如数组中根本没有与传入的元素相同的元素时;发挥你的英文能力,先猜一猜removeAt
方法是怎么执行删除的,没错,它是接受一个索引,删除指定索引处的元素,然后将这个索引之后的元素向前移动一位,还是像排队一样,前面的人离开了,后面的人依次前进一位。使用示例:
1 | List<int> lis = [1,2,3,4,4,5]; |
还有更多高级的数组操作请自行搜索,这里只介绍最基本。
数组的遍历,所谓遍历,就是将数组中的元素依次处理一遍,我们会在循环那一节详细介绍,这里给大家一段代码感受一下:
1 | List<int> lis = [1,2,3,4,5]; |
Map类型
在日常生活中,我们经常需要将一个事物和另一个事物关联起来,比如“姓名”和“电话号码”的对应关系,或者“物品”和“价格”的对应关系。在编程中,我们也需要用一种方式将不同的数据配对存储起来。Dart 提供了一种很常用的数据结构,叫做 Map,它可以帮助我们存储和查找这种一对一的关系。
什么是 Map?
你可以把 Map 想象成一个字典,里面有很多“词条”,每个词条都有“单词”和它的“定义”。在 Dart 中,这两个部分分别叫做 键(Key)和 值(Value)。键是用来查找的,值是你想要存储的数据。
举个例子:
键(Key) | 值(Value) |
---|---|
苹果 | 红色 |
香蕉 | 黄色 |
蓝莓 | 蓝色 |
在这个例子里,“苹果”是键,它的值是“红色”。每次我们输入“苹果”这个键,就能查到它对应的颜色——“红色”。
创建一个 Map
我们来看看在 Dart 里如何创建一个 Map。创建 Map 的语法非常简单,你只需要用大括号 {}
,然后在里面写上你想存储的键值对:
1 | var fruits = { |
这段代码创建了一个名为 fruits
的 Map,其中包含了三组水果与颜色的对应关系。
如何使用 Map
- 访问值想要查找一个键对应的值时,只需要使用方括号
[]
并输入对应的键:
1 | print(fruits['苹果']); // 输出: 红色 |
- 添加或更新值如果你想往 Map 中添加一个新的键值对,或者更新已有的键对应的值,你可以这样做:
1 | fruits['葡萄'] = '紫色'; // 添加新的键值对 |
- 删除值如果你想从 Map 中删除某个键值对,可以使用
remove
方法:
1 | fruits.remove('香蕉'); // 删除“香蕉” |
- 查看 Map 的大小如果你想知道 Map 中有多少个键值对,可以使用
length
属性:
1 | print(fruits.length); // 输出: 3 |
Map 的实际用途
你可以用 Map 来做很多有趣的事情。举个例子,假设你正在编写一个小程序,用来记录班级每个同学的分数。你可以使用 Map 来存储每个同学的名字和他们的分数:
1 | var studentScores = { |
这样,每次你需要查询某个同学的分数时,只需要输入他们的名字作为键,就可以快速得到对应的分数。
运算符
1. 算术运算符
算术运算符用来进行数学计算,比如加法、减法、乘法等。
运算符 | 作用 | 示例 | 结果 |
---|---|---|---|
+ |
加法 | 5 + 3 |
8 |
- |
减法 | 10 - 4 |
6 |
* |
乘法 | 6 * 7 |
42 |
/ |
除法(返回小数) | 10 / 2 |
5.0 |
% |
取余(取模运算) | 10 % 3 |
1 |
~/ |
整数除法(舍去小数) | 10 ~/ 3 |
3 |
示例:
1 | void main() { |
2. 赋值运算符
赋值运算符用来将值赋给变量。最常见的赋值运算符是 =
,它表示将右边的值赋给左边的变量。
运算符 | 作用 | 示例 | 结果 |
---|---|---|---|
= |
赋值 | a = 5 |
变量 a 等于 5 |
+= |
加后赋值 | a += 3 |
a = a + 3 |
-= |
减后赋值 | a -= 2 |
a = a - 2 |
*= |
乘后赋值 | a *= 4 |
a = a * 4 |
/= |
除后赋值 | a /= 2 |
a = a / 2 |
示例:
1 | void main() { |
3. 比较运算符
比较运算符用来比较两个值。它们的结果是 true
或 false
,即要么是“真”,要么是“假”。
运算符 | 作用 | 示例 | 结果 |
---|---|---|---|
== |
等于 | 5 == 5 |
true |
!= |
不等于 | 5 != 3 |
true |
> |
大于 | 6 > 4 |
true |
< |
小于 | 3 < 5 |
true |
>= |
大于或等于 | 7 >= 7 |
true |
<= |
小于或等于 | 5 <= 10 |
true |
示例:
1 | void main() { |
4. 逻辑运算符
逻辑运算符用来处理逻辑判断,比如 AND
(与)、OR
(或) 和 NOT
(非)。这些运算符常用在条件判断中,配合布尔值 (true
或 false
) 使用。
运算符 | 作用 | 示例 | 结果 |
---|---|---|---|
&& |
逻辑与(AND) | true && false |
false |
` | ` | 辑或(OR) | |
! |
逻辑非(NOT) | !true |
false |
示例:
1 | void main() { |
5. 增量/递减运算符
增量运算符和递减运算符用来增加或减少变量的值。++
用来将变量增加 1,--
用来将变量减少 1。
运算符 | 作用 | 示例 | 结果 |
---|---|---|---|
++ |
增加 1 | a++ 或 ++a |
a = a + 1 |
-- |
减少 1 | a-- 或 --a |
a = a - 1 |
示例:
1 | void main() { |
6. 类型判定运算符
类型判定运算符用来检查变量的类型。它们帮助我们判断某个变量是否属于某种数据类型。
运算符 | 作用 | 示例 | 结果 |
---|---|---|---|
is |
是否属于某种类型 | a is int |
true 或 false |
is! |
是否不属于某种类型 | a is! String |
true 或 false |
示例:
1 | void main() { |
变量声明
- 直接使用类型来声明变量
Dart也是可以直接用类型来声明变量的,就像你在Java和C/C++中做的那样:
1 | int? a; // 声明一个整型变量 |
我们在真正的开发中****优先选择这种方式******来声明变量,这样会大大加强代码的可读性,不会造成困惑,并且还有利于编辑器为你提供完善的代码提示,提高写代码的效率。
你可能会疑惑类型后面的问号是什么,这个跟空安全有关,点击这里跳转到空安全一节
var
关键字
使用var
关键字声明的变量算是"半个"动态类型,因为这个变量的类型将会在变量第一次被赋值后根据被赋值的值的类型来确定,例如:
1 | var my = "Hello"; // 此时my变量的类型被确定为String类型 |
dynamic
和Object
在Dart中,Object
是所有类的基类,包括int
类型等基本类型。所以可以用Object
类型的变量来存放任何类型的值。dynamic
关键字声明的变量也能接受任意类型的值。dynamic
和Object
没有var
那样的限制,在赋值了一种类型的值过后还能继续赋值其他类型的值,是真正意义上的动态类型。
两者的区别在于,由于Object类型的变量可以接受任意类型的值的原理是它是所有类型的父类,正因为如此,它不能调用其子类的方法,否则就会报错,例如:
1 | dynamic a; |
length
是String类型定义的一个getter
方法,Object
中并无此方法的定义,所以即使用Object
类型的变量来接收String
类型的值,也无法调用String
类的方法以及访问其属性。但是从上述例子看出,dynamic不存在这个问题,所以在实际的开发中会用到动态类型的地方,我们一般都是使用dynamic
。
加油啊,你马上就要把变量声明看完了!!!
final
和const
如果从未打算更改一个变量,那么使用 final
或 const
,不是var
,也不是一个类型。 一个 final
变量只能被设置一次,两者区别在于:const
变量是一个编译时常量(编译时直接替换为常量值),final
变量在第一次使用时被初始化。被final
或者const
修饰的变量,变量类型可以省略,如:
1 | //可以省略String这个类型声明 |
恭喜进入新篇章!
条件判断
if
语句
if
语句表示“如果某个条件成立,就执行某段代码”。
1 | int age = 18; |
上面的代码意思是:“如果age
的值大于或等于18,输出‘你是成年人’”。
if-else
语句
if-else
语句增加了“否则”的选项,如果条件不成立,就会执行else
后面的代码。
1 | int age = 16; |
这里,如果age
小于18,程序会输出‘你未成年’。
else if
语句
else if
用来处理多个条件,依次检查,直到找到一个成立的条件。
1 | int score = 85; |
这段代码先检查score
是否大于等于90,如果不是,再检查是否大于等于60,最后才执行“否则”的情况。
三元运算符
三元运算符是简化的条件判断,适用于简单的“如果…否则”情况。
1 | int age = 20; |
这里的age >= 18
是判断条件,如果为真,message
的值为‘成年人’,否则为‘未成年’。
获得武器:三叉戟
循环
for
循环
for
循环在你知道循环次数的情况下使用最方便。它由三部分组成:初始化、条件和迭代,可以理解为“从哪里开始,到哪里结束,以及每次要做什么”。
1 | for (int i = 0; i < 5; i++) { |
这段代码的意思是:“从 i = 0
开始,执行 i < 5
这个条件,每次循环之后把 i
加 1”,输出结果是:
1 | 0 |
循环从 0 开始,输出到 4,因为当 i = 5
时,条件 i < 5
不再成立,循环结束。
while
循环
while
循环在你不确定循环次数时使用最合适,它会一直重复执行代码,直到某个条件不再满足。
1 | int i = 0; |
这段代码和 for
循环类似,输出也是从 0 到 4。while
循环的核心是:“只要条件 i < 5
成立,就一直执行里面的代码”,并在每次循环后将 i
加 1。
达成成就:转呀转
哼哼,看来循环是难不住你了,来点劲大的!❤️🔥
空安全
空安全的内容建议大家牢固掌握,后面会多次涉及
Dart 中一切都是对象,这意味着如果我们定义一个数字,在初始化它之前如果我们使用了它,假如没有某种检查机制,则不会报错,比如:
1 | test() { |
在 Dart 引入空安全之前,上面代码在执行前不会报错,但会触发一个****运行时错误******,原因是 i 的值为 null 。但现在有了空安全,则定义变量时我们可以指定变量是可空还是不可空。
1 | int i = 8; //默认为不可空,必须在定义时初始化。 |
如果一个变量我们定义为可空类型,在某些情况下即使我们给它赋值过了,但是预处理器仍然有可能识别不出,这时我们就要显式(通过在变量后面加一个”!“符号)告诉预处理器它已经不是null了,比如:
1 | class Test{ |
null:表示什么都没有,在C/C++和Java中都有这个概念
上面中如果函数变量可空时,调用的时候可以用语法糖:
1 | fun?.call() // fun 不为空时则会被调用,否则不会被调用 |
其实空安全没有那么复杂,就一句话:不可空类型的变量能被100%确定不是null.
无论是使用late关键字也好,还是给它一个初始值也好,这些都是让编译器确定这个变量一定不是null的方式。
**达成成就:色即是空 **
沃趣,空安全这么前沿的特性都被你掌握了!牛蛙牛蛙!!
函数
Dart是一种真正的面向对象的语言,所以即使是函数也是对象,并且有一个类型Function。这意味着函数可以赋值给变量或作为参数传递给其他函数,这是函数式编程的典型特征。
函数声明
1 | bool isNoble(int atomicNumber, double i) { |
Dart函数声明如果没有显式声明返回值类型时会默认当做dynamic
处理,注意,函数返回值没有类型推断:
1 | typedef bool CALLBACK(); |
对于只包含一个表达式的函数,可以使用简写语法:
1 | bool isNoble (int atomicNumber)=> true ; |
函数作为变量
1 | var say = (str){ |
函数作为参数传递
1 | //定义函数execute,它的参数类型为函数 |
我们将被作为参数传递的函数称为回调函数, 其含义在于,这个被传入的函数不是由你自己去调用,而是在其他地方被另一个函数调用,例如上方代码的第三行就调用了从外部传入进来的函数,这个概念很重要,在Flutter的开发中,我们会经常自己编写回调函数,乃至调用回调函数的函数(有点拗口,就是指的上例中的execute函数
函数的参数
位置参数
位置参数是按顺序传递给函数的参数。比如你有一个函数,它有两个参数,调用这个函数时,必须按照函数定义中的顺序来传递参数。
1 | void greet(String name, int age) { |
在上面的代码中,函数 greet
有两个位置参数 name
和 age
,调用时,必须先传入名字,然后传入年龄,输出为:
1 | 你好,小明,你今年 20 岁了。 |
可选的位置参数
包装一组函数参数,用[]标记为可选的位置参数,并放在参数列表的最后面:
1 | String say(String from, String msg, [String? device]) { |
下面是一个不带可选参数调用这个函数的例子:
1 | say('Bob', 'Howdy'); //结果是: Bob says Howdy |
下面是用第三个参数调用这个函数的例子:
1 | say('Bob', 'Howdy', 'smoke signal'); //结果是:Bob says Howdy with a smoke signal |
可选的命名参数
定义函数时,使用{param1, param2, …},放在参数列表的最后面,用于指定命名参数。例如:
1 | //设置[bold]和[hidden]标志 |
调用函数时,可以使用指定命名参数。例如:paramName: value
1 | enableFlags(bold: true, hidden: false); |
可选命名参数在Flutter中使用非常多。注意,不能同时使用可选的位置参数和可选的命名参数。
你能打败接下来这个精英怪吗?!
匿名函数
顾名思义,匿名函数就是没有名字的函数,有以下两种方式可以定义匿名函数:
- 大括号法
1 | final f = () { |
- 箭头法
我们知道,有些函数是有返回值的,对于只有一行代码,且这行代码是返回语句的函数来说,可以使用箭头函数法来书写,这与上面的大括号法没有什么区别,只不过是语法上更加简洁而已
1 | final f = () => "Hello World!"; // 同样的,应该写上分号 |
: 你这叫哪门子匿名函数,不是还得用一个有名字的变量来接收它吗?
这个问题问得好,我们来看看以下这个例子
1 | // 一个普通的函数 |
看明白了吗,我们可以把匿名函数当作参数传入到另一个函数里面去,然后由另一个函数去调用,这个过程中,对于使用proxyInvoke
函数的我们来说,我们传入的函数就是匿名的,只不过proxyInvoke
能在其内部通过一个名为f
的变量来调用这个函数而已。
达成成就:我倒要看看你是什么🌳
这么强??居然能走到这里,那再给你安排一个大BOSS!
类(Class)
类是面向对象编程的重要概念,你可以把类理解成一个可以存放数据和方法(放在类里面的函数称为方法)的容器就可以了。那么类是有实例的,实例就是类的一个个具象化表现,一个类可以有多个实例。
打个比方,当我们谈论食物的时候,我们到底在谈论什么呢(bushi。食物是一个抽象的东西,我们不可能见到一个真正存在的一个叫食物的物体,我们能见到米饭,面条等等,都是食物这个类的实例,这些实例有自己的特点,但是也有一些相似的地方,那就是可以被人食用并且还有一定的营养。将一堆东西相似的地方抽象出来后的产物,就可以被称为类。你如果了解过哲学的话,可以回想一下《理想国》里面的内容(
类的构造函数和属性
在Dart中,通过class
关键字来定义类:
1 | class User{ |
一个类可以拥有构造函数和成员变量(又称属性),构造函数的名字和类名相同且没有返回值。这里要注意,只要你的成员变量不是可空类型(即后面跟了一个问号的类型,如果你忘了再去看看前面的空安全)那么都必须在构造函数中给它指定一个初始值,这个初始值来自于外部传入,在上例中,我们传入的是"Tom"和23。我们可以通过它的构造函数来创建一个实例对象(上述代码第8行和第9行)。我们可以看到,即使传入的成员变量的值一样,这两个实例仍然是不同的两个对象。
在Dart中,构造函数的参数可以写成位置参数的形式,也能写成命名参数的形式,如果你忘了什么是位置参数,什么是命令参数,回到函数的参数一节复习一下,上例中我们用的是位置参数的形式,注意要在你想赋值的参数前加一个this
,这是语法上强制要求的。
我们还可以使用命名参数的形式,这是我们在Flutter开发时经常使用的形式,所以需要重点掌握,将上述例子改写成命名参数的形式:
1 | class User{ |
命名参数是可选的,但由于name
和age
是不可空类型,所以我们必须在定义构造函数的时候在name
和age
前加上一个required
,表示这个参数必须要传入,如果不传入,就会报编译错误:
上面编译器提示我们一个名为name
的参数还没有传入。
我们在定义类时,可以采取这种方式来保证我们的类所必须的变量有一个合法的值,而不是空,这也是空安全特性带给我们的好处之一。除此之外,类的定义还能和可空类型结合,下面是一些例子:
1 | class ClassA{ |
如果你对上面涉及到的空安全感到疑惑,可以看看这句话,使用required
关键字和赋予默认值都是保证其不可空的方式,无论什么方式,只要能确定一个变量不为空,那么它就可以被声明为不可空类型
成员方法
在类中还能定义函数,我们把定义在类中的函数称为方法:
1 | class User{ |
访问类的成员
类的属性和方法统一称为类的成员,基本上在所有编程语言中,访问类的成员都使用”点取“,Dart也不例外,以上述代码为例,我们如果要访问User类中的属性和方法,我们先要创建一个User的实例:
1 | // 创建实例 |
达成成就:初见对象(
私有成员
私有成员,顾名思义,就是外部不能直接使用点取来访问的成员,在C++和Java中,你可能会使用private
关键字来声明私有成员,而在Dart中,一个名字以下划线开头的成员,就是私有成员:
1 | class User{ |
可以看到,我们通过User类的实例直接点取_birthYear
属性是行不通的,但是我们可以使用getBirthYear()
方法来获取到_birthYear
的值,毕竟这个方法不是私有的。所以,私有成员是可以通过公有成员被外界间接访问的。那为什么要多此一举呢,这在面向对象编程中被称为封装,封装有很多好处,随着大家编程经验的慢慢丰富,会感受到的
当然,方法也是可以被定义成私有的,从而防止外部直接访问,只需在名字前加一个下划线即可,这里不再赘述
DateTime是Dart内置的一个类,DateTime.now()可以返回一个包含当前时间的对象,通过其year属性,可以获取当前是多少年
静态成员
我们上面获取类中成员之前,都需要创建一个该类的实例,但对于静态成员不同,我们可以直接通过类名来访问静态成员(当然,通过实例来访问也是可以的),Dart中使用static
关键字来定义静态成员:
1 | class User{ |
可以看到,我们没有创建User
的实例,也能直接访问sayHello
方法和userCount
变量
我们创建了一个名为userCount
的静态变量,用来记录当前用户个数,每当调用构造函数时,userCount
就加1,所以我们最后会看到输出User.userCount
时是2,因为我们调用了两次构造函数。
同时最后两行代码还表明了,静态成员是所有实例共同拥有的,它只存在一份,而不会像普通成员变量那样,每个实例都有一份自己的。
继承
在编程中,有时候我们会遇到一些事物,它们虽然不同,但有许多相似的地方。为了避免重复编写相似的代码,我们可以使用一种机制,叫做 继承。通过继承,一个类可以“继承”另一个类的属性和方法,从而复用已有的功能。
继承的概念其实跟现实生活中的继承很像:子女从父母那里继承特性,例如眼睛的颜色、身高等。在 Dart 中,一个类(子类)可以继承另一个类(父类)的功能,并且在这个基础上添加或修改一些功能。
什么是类的继承?
类的继承 是一种面向对象编程的机制,允许我们创建一个新类,并从现有的类那里获取它的一些特性(比如属性和方法)。继承让我们可以避免重复代码,同时可以更灵活地扩展类的功能。
举个例子
假设我们正在编写一个关于动物的程序。每种动物都有一些相似的特性,比如都会“吃东西”、“睡觉”。但同时,每种动物也有自己的特性,比如“狗会叫”,“鸟会飞”。
我们可以创建一个通用的 Animal
类,包含所有动物共有的行为,然后让具体的动物类(比如 Dog
和 Bird
)继承这个 Animal
类。
1 | // 创建一个父类 Animal |
在这个例子里,Dog
和 Bird
都继承了 Animal
类,因此它们拥有了 Animal
类中的 eat
和 sleep
方法。同时,它们也各自有自己的独特方法,比如 Dog
有 bark
方法,Bird
有 fly
方法。
我们可以这样使用这些类:
1 | void main() { |
通过继承,Dog
和 Bird
类不需要重复写 eat
和 sleep
的代码,它们自动拥有了这些功能。
重写父类的方法
在某些情况下,子类可能需要修改父类的方法。这个时候,我们可以使用 方法重写(override)。通过重写,子类可以提供自己对某个方法的实现,而不是使用父类的版本。
举个例子,虽然所有动物都会“吃东西”,但不同的动物吃东西的方式可能不一样。我们可以在 Dog
类中重写 eat
方法,让它展示狗特有的吃饭方式。
1 | class Dog extends Animal { |
现在,当我们调用 Dog
的 eat
方法时,它将输出“狗正在吃骨头”,而不是“动物正在吃东西”。
1 | void main() { |
super 关键字
有时候,子类可能想在重写方法的同时保留父类的部分实现。这时我们可以用 super
关键字来调用父类的方法。
1 | class Dog extends Animal { |
在这个例子中,super.eat()
调用了父类 Animal
中的 eat
方法,然后再添加了 print('狗吃得很开心')
。这使得 Dog
类既保留了父类的行为,又添加了自己的行为。
1 | void main() { |
继承的实际应用
继承在很多应用场景中都很有用。比如,你在开发一个游戏,游戏里有很多不同类型的角色:玩家、敌人、NPC(非玩家角色)。这些角色可能有一些共同的行为,比如移动、攻击。你可以创建一个 Character
父类,包含这些共有的功能,然后让每个具体的角色继承这个类,并添加各自特有的行为。
1 | class Character { |
泛型
在编程中,我们常常会遇到一种问题:如何编写既能处理数字,又能处理字符串、甚至其他数据类型的代码?泛型 (Generics) 就是用来解决这种问题的工具。它允许我们编写“通用”的代码,而不必针对不同的数据类型重复编写代码。
什么是泛型?
简单来说,泛型 就是给某些数据结构或者函数加上“类型参数”,使它们能适应不同的数据类型。它就像是一个“模板”,允许你在使用时指定具体的数据类型,而不用一开始就固定下来。
假设你有一个 箱子,你可以在其中存放不同的东西。我们可以把 箱子 设计成“泛型”,这样它可以在不修改本身设计的情况下,容纳不同类型的物品,比如苹果、书本、或者其他东西。泛型的好处就是让这个 箱子 具备了灵活性。
在 Dart 中,我们可以用泛型来让数组、类、函数适应多种不同类型。
如何使用泛型
- 泛型的基本语法
假设我们有一个数组想要存储一些整数,我们可以使用泛型来指定这个数组只能存储 int
类型的元素:
1 | List<int> numbers = [1, 2, 3, 4]; |
List<int>
表示这个数组只能存储整数类型的数据。如果你尝试将非整数的数据放进去,Dart 会报错。
同样的道理,如果我们想存储字符串,可以使用 List<String>
:
1 | List<String> names = ['Alice', 'Bob', 'Charlie']; |
- 泛型函数
泛型不仅可以用于数组,还可以用于函数。如果你有一个函数,它的逻辑是通用的,可以适用于多种类型的数据,那么你可以使用泛型来实现这个函数。
比如我们有一个函数,用来返回列表中的第一个元素。这个函数不管是处理整数列表、字符串列表,还是其他类型的列表,逻辑都是一样的。我们可以用泛型来编写:
1 | T getFirst<T>(List<T> items) { |
在这个例子中,T
就是泛型类型参数。它表示这个函数可以处理任何类型的数据。我们在使用时,可以传入不同类型的列表,Dart 会自动知道 T
应该是什么类型:
1 | print(getFirst([1, 2, 3])); // 输出: 1 |
- 泛型类
除了函数,我们还可以使用泛型来创建类。如果你正在设计一个类,想让它能处理多种不同类型的数据,就可以用泛型来实现。
假设我们要设计一个容器类 Box
,这个容器可以存放任何类型的数据,我们可以使用泛型来实现这个类:
1 | class Box<T> { |
在这个 Box
类中,T
表示可以存放的任何类型。我们可以用 Box<int>
来创建一个存放整数的盒子,也可以用 Box<String>
来存放字符串。
1 | Box<int> intBox = Box(123); |
达成成就:速通Dart基础部分!
异常
在编程中,错误和问题是不可避免的。当程序遇到某些意外情况时,它可能会中断运行。为了防止这些情况导致程序崩溃,我们可以使用 异常处理 来应对意外的错误。
在 Dart 中,异常处理机制允许我们捕获并处理这些错误,从而保证程序继续运行,而不是直接崩溃。今天,我们将介绍 Dart 中的异常处理,以及如何优雅地应对程序中的问题。
什么是异常?
异常 是一种信号,表示程序运行过程中发生了某种错误或不正常的情况。例如:
- 试图打开不存在的文件。
- 数字除以 0。
- 从网络请求中未能获取数据。
当这些异常发生时,程序默认会终止运行。但是通过异常处理,我们可以提前预见这些问题并处理它们,避免程序直接崩溃。
Dart 中的异常处理
在 Dart 中,异常通过 try-catch
机制进行处理。我们将可能发生异常的代码放在 try
代码块中,如果异常发生,程序会跳转到 catch
代码块进行处理。
1. try-catch
语法
try-catch
的基本结构如下:
1 | try { |
try
块中包含可能引发异常的代码。catch
块用来处理捕获到的异常,e
代表发生的异常信息。
示例:简单的 try-catch
1 | void main() { |
输出:
1 | 发生异常: IntegerDivisionByZeroException |
在这个例子中,10 ~/ 0
会引发除以 0 的异常,程序不会崩溃,而是跳到 catch
块处理异常,并输出异常信息。
2. on
关键字
有时候我们想要针对特定类型的异常做出不同的处理。这时可以使用 on
关键字,它可以用来捕获特定类型的异常。
1 | void main() { |
输出:
1 | 无法除以 0 |
在这个例子中,on IntegerDivisionByZeroException
捕获了除以 0 的异常,输出了特定的错误信息。如果发生其他类型的异常,程序会跳到 catch
块。
3. finally
块
有时我们希望无论是否发生异常,都能执行某些代码。比如,关闭文件、断开数据库连接等操作。此时可以使用 finally
块。
finally
块中的代码无论是否有异常发生,都会执行。
1 | void main() { |
输出:
1 | 5 |
在这个例子中,虽然没有异常发生,但 finally
块中的内容依然被执行。如果发生异常,finally
也会照常执行。
4. throw
关键字
有时,我们希望手动引发异常。比如在特定情况下,如果数据不符合预期,我们可以使用 throw
关键字抛出一个异常,来提醒程序处理异常情况。
1 | void checkValue(int value) { |
输出:
1 | 捕获到异常: Exception: 值不能是负数 |
在这个例子中,我们使用 throw
手动抛出异常,当检测到传递的值是负数时,程序跳到 catch
块进行处理。
5. 自定义异常
Dart 允许我们创建自己的异常类,继承自内置的 Exception
类。通过自定义异常,我们可以为自己的程序创建特定的错误类型,便于更精准地处理不同的错误。
1 | class NegativeValueException implements Exception { |
输出:
1 | 值不能是负数 |
在这个例子中,我们定义了一个 NegativeValueException
异常类,并在 checkValue
函数中使用它抛出错误。当值为负数时,会抛出自定义的异常,并捕获并打印错误消息。
异步编程
在日常生活中,很多事情都是并行发生的,比如一边煮饭,一边看书。这种“同时进行”的概念在编程中叫做 异步编程。异步编程允许程序在等待某些操作完成时,继续执行其他任务,而不会被某个耗时操作阻塞。
在 Dart 中,异步编程可以用 Future
类型和 async
、await
关键字来实现。
为什么需要异步编程?
当程序执行一些耗时操作,比如从网络上获取数据、读取文件或者执行复杂的计算时,程序会停下来等待这些操作完成。如果我们用 同步 方式写代码,程序在等待时会暂停其他任务,导致效率低下。
异步编程 允许程序继续执行其他任务,而不会被这些耗时操作卡住。例如:
- 在从网络获取数据时,程序可以继续处理用户输入。
- 在等待文件读取时,程序可以同时更新界面。
Future
类型
在 Dart 中,异步操作的结果通常是一个 **Future**
类型的对象。Future
代表未来某个时刻会完成的操作,它的结果可能是成功的,也可能是失败的。
想象一下,Future
就像我们点了一份外卖。我们不知道外卖什么时候送到,但我们可以继续做别的事情,直到外卖到达(操作完成)。这就是 Future
的作用:它承诺未来某个时刻会给你结果。
async
和 await
关键字
为了让异步操作更容易理解和书写,Dart 提供了两个关键字:**async**
和 **await**
。
async
:用来标记一个函数为异步函数,表示这个函数内部可能有耗时操作,执行这些操作时不会阻塞其他代码。await
:用来等待异步操作的完成。在await
后面的代码只有在异步操作完成后才会执行。await关键字只能在async标记的函数中使用
示例:基本异步操作
1 | Future<String> fetchData() async { |
输出:
1 | 开始获取数据... |
在这个例子中:
fetchData
是一个返回Future<String>
的异步函数,里面用await
等待了 2 秒的模拟延迟。- 当我们调用
fetchData
时,程序不会被卡住,而是继续执行其他任务。在await
之后,程序会等待fetchData
函数返回结果,输出数据获取完成
。
异步函数如何工作?
异步函数的执行方式和普通函数不同:
- 当我们调用异步函数时,程序会立即返回一个
Future
对象,表示操作正在进行中。 - 如果此时使用了await关键字,程序就会暂停,等待这个操作完成,然后再继续执行后面的代码。
示例:不使用 await
的情况
如果我们不使用 await
来等待异步操作,程序会立即返回 Future
对象,而不会等待结果。
1 | Future<String> fetchData() async { |
动用你的英语能力猜一猜上面的
then
表示什么
输出:
1 | 开始获取数据... |
在这个例子中,程序在调用 fetchData
后,并没有等待结果,而是立即继续执行其他任务。只有当 fetchData
完成后,结果才通过 then
被打印出来。
错误处理:try-catch
和 Future
的错误处理
在异步操作中,可能会发生错误。例如,网络请求失败、文件读取出错等。为了处理这些错误,我们可以在 await
语句前使用 try-catch
,或者直接处理 Future
的 catchError
方法。
使用 try-catch
处理错误
1 | Future<String> fetchData() async { |
使用 catchError
处理错误
1 | Future<String> fetchData() { |
何时使用异步编程?
在以下场景中,使用异步编程会让程序更加高效:
- 网络请求:当程序从服务器获取数据时,网络延迟不可避免,因此使用异步编程不会让界面卡住。
- 文件操作:读取或写入大文件时,使用异步操作可以提高程序的响应速度。
- 等待用户输入:如果某个操作需要用户输入或等待用户点击按钮,异步编程可以让程序继续执行其他任务。