版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.11.08 星期五 |
前言
Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。 Flutter可以与现有的代码一起工作。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的。目前公司的部分模块就是在使用Flutter进行开发。感兴趣的可以看下面几篇文章。
1. Flutter开发技术与分享(一) —— 基本概览(一)
2. Flutter开发技术与分享(二) —— Flutter 入门(一)
开始
首先看下主要内容
主要内容:了解
Dart
编程语言的基础知识,该语言可用于Flutter SDK进行移动,Web和其他应用开发。翻译地址。
接着看下写作环境
Dart 2, Flutter, DartPad
您正在寻找Flutter开发入门,但只是发现Flutter使用Dart
编程语言吗?本教程将向您介绍一些Dart基础知识,以便您可以起步并使用Flutter开发语言。
Dart
由Google
于2011年发布,最初旨在替代JavaScript
。从那时起,针对iOS,Android和Web开发的Flutter SDK的发布使Dart
语言成为新的焦点。录制时,Dart的最新版本是2.4
。
Dart
与您可能已经熟悉的其他语言有很多相似之处,例如Java,C#,Swift和Kotlin
。 Dart具有以下语言特征:
Statically typed
Type inference
String expressions
Multi-paradigm including OOP and functional
Can mimic dynamic types using dynamic
除了用于移动和Web开发之外,Flutter
还是针对Fuchsia的开发套件,Fuchsia
是Google
正在开发的实验性操作系统。
本教程将向您介绍Dart基础知识,例如:
Variables, data types, and operators
Conditionals and loops
Functions, anonymous functions, and closures
Arrow function syntax
您可以在本课程不仅仅了解基础知识,还可以了解有关Dart中的面向对象编程以及Dart集合类型(如列表,集合和maps
)的更多信息。
完成本教程后,您应该已经准备好使用Dart
直接进入Flutter
开发。
为了快速起步并学习Dart,最好的方法是使用DartPad编辑器,该编辑器位于 https://dartpad.dartlang.org:
DartPad的安装类似于典型的IDE。 有:
- 左侧的“编辑器”
Editor pane
窗格 - 运行
(Run)
按钮以在编辑器中运行代码 - 右上方的控制台
(Console)
显示输出 - 右下角的“信息”
(Info)
面板显示信息突出显示的代码 -
Samples
下拉列表显示一些示例代码 - 共享
(Share)
按钮,可用于共享已创建的DartPad
文件
在右下角的下方,有一个复选框,显示更多与使用Dart
进行网络编程有关的面板,以及一些文本,显示DartPad
中使用的Dart当前版本。
如果愿意,可以在计算机上本地安装Dart SDK
。 一种方法是安装Flutter SDK。 安装flutter也将安装Dart SDK
。
您也可以直接访问此处here直接安装Dart SDK
。
Core Concepts
Dart
程序以对main
函数的调用开始,并且main的语法与其他语言(例如C,Swift或Kotlin)的main语法非常相似。
清除默认DartPad
中的所有代码,并向编辑器添加一个main
函数:
void main() {
}
main
函数之前有一个返回类型,在Dart
中,该类型是void
,表示不返回任何内容。 main
后面的括号表示这是一个函数定义,花括号包含函数的主体。 在main
内部,为程序添加Dart
代码。
1. Variables, Comments, and Data Types
我们要添加到main
的第一件事是变量赋值语句。 变量保存程序将要处理的数据。 您可以想到一个变量,例如计算机内存中的一个包含值的盒子。 每个盒子都有一个名称,即变量的名称。 您在Dart中使用var
关键字表示变量。
向main
添加一个新变量:
var myAge = 35;
像C和Java一样,每个Dart
语句都以分号结尾。 您已经创建了一个myAge
变量,并将其设置为35
。
您可以使用Dart中的内置print
函数将变量打印到控制台,因此请在变量之后添加该调用:
print(myAge); // 35
点击DartPad
中的Run
按钮以运行代码,然后您将在右上角的控制台中看到打印出的变量。
Dart
中的注释看起来与其他语言中的C中的注释类似:在一行上//后面的文本,或/ … /块内的文本。
// This is a comment.
print(myAge); // This is also a comment.
/*
And so is this.
*/
如果将光标放在myAge
名称上方的编辑器中,您将在右下面板中看到Dart
推断myAge
是一个int
变量,这要归功于它是用整数值35初始化的。像Swift
和Kotlin
一样,Dart使用 如果您未直接指定数据类型,则使用类型推断来尝试找出数据类型。
使用类型推断的另一种方法是将var
关键字替换为要使用的特定类型:
int yourAge = 27;
print(yourAge); // 27
与C,Java,Swift
和Kotlin
等许多语言类似,Dart
是静态类型的。 这意味着Dart中的每个变量都具有在编译代码时必须知道的类型,并且在程序运行时不能更改变量的类型。
这与动态类型化的语言(例如Python
和Javascript
)形成对比,这意味着变量在运行时可以保存不同种类的数据,并且在编译代码时不需要知道类型。
Basic Dart Types
Dart
使用int
表示整数,使用double
表示浮点,使用bool
表示布尔值。 int和double均源自名为num
的类型。 您使用String
类型表示字符序列。 Dart还具有关键字dynamic
,可让您模仿静态类型的Dart中的动态类型。
您还可以对int
以外的类型使用类型推断。 输入等于3.14
的变量pi
,如下所示:
var pi = 3.14;
print(pi); // 3.14
pi
被推断为双精度,因为您使用了浮点值对其进行了初始化。 您可以在Dart
信息面板中看到
您也可以只使用double
而不是var
来作为类型:
double c = 299792458;
print(c); // 299792458
在这种情况下,您已经将光速符号c
初始化为int
,但是由于您将类型指定为double
,因此c
实际上是double
。 Dart
会将int
转换为double
以便初始化c
。 因此,与Swift不同,Dart具有隐式类型转换 (implicit type-conversion)
。
The dynamic Keyword
如果使用dynamic
关键字而不是var
,则可以得到有效地是动态类型的变量:
dynamic numberOfKittens;
您可以使用引号将numberOfKittens
设置为String
(有关String
类型的更多信息,请参见下文)
numberOfKittens = 'There are no kittens!';
print(numberOfKittens); // There are no kittens!
numberOfKittens
具有类型,因为Dart
具有静态类型,所以它必须具有类型。 但是该类型是dynamic
的,这意味着您可以为其分配具有其他类型的其他值。 因此,您可以在打印语句下方分配一个int
值。
numberOfKittens = 0;
print(numberOfKittens); // 0
或者,如果您可以指定一个双精度值:
numberOfKittens = 0.5;
print(numberOfKittens); // 0.5
继续并单击Run
,以查看在控制台中打印出的numberOfKittens
的三个不同值。 在每种情况下,即使变量本身包含不同类型的值,numberOfKittens
的类型仍保持dynamic
。
布尔类型用于保存true
或false
的值。
bool areThereKittens = false;
print(areThereKittens); // false
但是,如果您也可能进行如下修改:
numberOfKittens = 1;
areThereKittens = true;
print(areThereKittens); // true
再次运行代码以在控制台中查看布尔值。
2. Operators
Dart
具有您熟悉的其他语言所有常用操作符,例如C,Swift和Kotlin。
有算术运算符,相等,递增和递减,比较和逻辑运算符。
Dart还允许像C ++
和Kotlin
这样的运算符重载,但这超出了本教程的范围。
算术运算符的工作就像您期望的那样。 向DartPad添加一堆操作:
print(40 + 2); // 42
print(44 - 2); // 42
print(21 * 2); // 42
print(84 / 2); // 42
这里有更多。
您可以使用算术表达式来初始化变量:
var atltuae = 84.0 / 2;
print(atltuae); // 42
Dart
在操作之前将int
转换为double
,以便将结果变量推断double
。
Dart
具有双等号相等和非等号运算符:
print(42 == 43); // false
print(42 != 43); // true
固定前和固定后增量和减量运算符:
print(atltuae++); // 42
print(--atltuae); // 42
因为增量使用的是后缀,所以在增量发生之前先打印42
。 减量为前缀,因此将43
减为42
,然后打印值42
。
Dart
具有典型的比较运算符,例如小于和大于或等于。
print(42 < 43); // true
print(42 >= 43); // false
也有通常的复合算术/赋值运算符
atltuae += 1; print(atltuae); // 43
atltuae -= 1; print(atltuae); // 42
atltuae *= 2; print(atltuae); // 84
atltuae /= 2; print(atltuae); // 42
Dart
具有通常的模运算符。
print(392 % 50); // 42
逻辑运算符,例如&&
和||
或看起来像其他语言的语言一样。
print((41 < atltuae) && (atltuae < 43)); // true
print((41 < atltuae) || (atltuae > 43)); // true
负号运算符是感叹号,将false
转换为true
或者将true
转换为false
。
print(!(41 < atltuae)); // false
3. Strings
Dart
字符串类型为String
。 字符串在Dart中使用由单引号或双引号引起来的文本表示。
就像我们看到的其他类型一样,您可以使用var
和type inference
或String
来创建字符串变量:
var firstName = 'Albert';
String lastName = "Einstein";
与Kotlin
和Swift
等语言类似,您可以使用美元符号$
来将值和表达式嵌入字符串中以创建新的字符串。
var physicist = "$firstName $lastName";
print(physicist); // Albert Einstein
您可以组合相邻的字符串,例如,多行的长字符串,只需将字符串彼此相邻或放在单独的行上即可:
var quote = 'If you can\'t' ' explain it simply\n'
"you don't understand it well enough.";
print(quote);
// If you can't explain it simply
// you don't understand it well enough.
在第一个字符串中,您使用单引号,因此使用了转义序列\'
将不能插入的引号嵌入到字符串中。 Dart中使用的转义序列与其他类似C的语言中使用的转义序列相似,例如\ n
用于换行符。
由于使用了双引号来分隔第二个字符串,因此不需要为单引号在don't
上使用转义序列。
您还可以使用+
运算符组合字符串:
var energy = "Mass" + " times " + "c squared";
print(energy); // Mass times c squared
您可以使用三引号使字符串运行多行并保留格式:
var model = """
I'm not creating the universe.
I'm creating a model of the universe,
which may or may not be true.""";
print(model);
// I'm not creating the universe.
// I'm creating a model of the universe,
// which may or may not be true.
如果需要在字符串中显示转义序列,则可以使用以r
为前缀的原始字符串。
var rawString = r"If you can't explain it simply\nyou don't understand it well enough.";
print(rawString);
// If you can't explain it simply\nyou don't understand it well enough.
继续,然后单击DartPad
中Run
以在控制台中查看所有字符串。
4. Immutability
Dart
的关键字const
和final
表示不变的值。 const
用于在编译时已知的值,final
用于在编译时不必知道但在初始化后不能重新分配的值。 最后的行为,例如Kotlin
中的val
或Swift
中的let
。
您可以使用const
和final
代替var
,并让类型推断确定类型:
const speedOfLight = 299792458;
print(speedOfLight); // 299792458
因此,可以将speedOfLight
推断为int
,如您在DartPad
的信息面板中所见。
final
表示不可变,并且final
值不能重新分配。 您还可以使用final
或const
显式声明类型:
final planet = 'Jupiter';
// final planet = 'Mar'; // error: planet is immutable
final String moon = 'Europa';
print('$planet has a moon $moon');
// Jupiter has a moon Europa
5. Nullability
对于任何变量,无论类型如何,如果不初始化变量,则变量将被赋予null
,这意味着变量中将不存储任何内容。
这与诸如Swift
和Kotlin
之类的语言形成对比,对于这些语言,您必须将变量显式声明为nullable
(在Swift中又是optional
)。 这些语言的默认类型是不可为空(non-nullable)
的。
事实证明,所有Dart
类型,甚至包括基本类型(如int
和double
)都源自名为Object
的类型。 而且,如果您不初始化对象,则该对象将为空值(null)
。
您将在这里看到一些在Dart
中安全处理null
值的方法,但是它远不及Swift中的optionals
或Kotlin中的nullable
那样。
创建三个不同类型的变量,然后立即打印它们。
int age;
double height;
String err;
print(age); // null
print(height); // null
print(err); // null
您会在控制台中看到每个变量均为null
。
Dart
具有一些null-aware
的运算符,可用于处理null
值。
双问号运算符??
就像Kotlin
中的“ Elvis运算符”:如果不为空,它将返回左侧的操作数,否则返回右侧的值:
var error = err ?? "No error";
print(error); // No error
有一个对应的赋值运算符?? =
,其作用类似:
err ??= error;
print(err); // No error
由于您刚刚将error
non-null
设为“No error”
,因此err
值现在可以在此分配中获取该非空值。
还有一个运算符?.
保护您避免访问空对象的属性。 如果对象本身为null
,它将返回null
。 否则,它将返回右侧的属性值:
print(age?.isEven); // null
如果您仅尝试age.isEven
并且age
为null
,则将收到Uncaught
异常。
Control Flow
控制流使您能够确定某些代码行已执行,跳过或重复。 在Dart
中使用conditionals
和loops
来处理控制流。
1. Conditionals
控制流的最基本形式是根据程序运行时发生的情况来决定是执行还是跳过代码的某些部分。 用于处理条件的语言构造是if / else
语句。 Dart
中的if / else
与其他类似C的语言中的用法几乎相同。
假设您有一个动物变量,当前是狐狸。
var animal = 'fox';
您可以使用if
语句检查动物是猫还是狗,如果是,则运行一些相应的代码。
if (animal == 'cat' || animal == 'dog') {
print('Animal is a house pet.');
}
在这里,您已经使用了等于和或运算符在if
语句的条件内创建了一个bool
值。
如果条件为假,则可以使用else
子句运行替代代码:
} else {
print('Animal is NOT a house pet.');
}
// Animal is NOT a house pet.
您还可以将多个if / else
语句组合到一个if / else if / else
构造中:
if (animal == 'cat' || animal == 'dog') {
print('Animal is a house pet.');
} else if (animal == 'rhino') {
print('That\'s a big animal.');
} else {
print('Animal is NOT a house pet.');
}
// Animal is NOT a house pet.
如果需要,可以在if
和else
之间有很多else if
分支。
2. While Loops
循环使您可以重复编码一定次数或基于特定条件。 后者由while
循环处理。
Dart
中的while
循环有while
和do-while
两种形式。 区别在于,对于while
,循环条件在代码块之前,而在do-while
中,条件在之后。 因此,对于同时执行,可以保证代码块至少运行一次。
创建一个我初始化为1
的变量i
:
var i = 1;
您可以使用while
循环在递增i
的同时打印i,并将条件设置为i
小于10
:
while (i < 10) {
print(i);
i++;
}
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
运行代码,您会看到while
循环将数字1
到9
打印出来。
在DartPad
中重置i
,然后添加一个do-while
循环:
i = 1;
do {
print(i);
++i;
} while (i < 10);
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
此处的结果与之前相同,只是这次,在检查循环退出条件之前,循环主体运行了一次。
3. continue and break
Dart
具有通常在循环和其他地方使用的continue
和break
关键字。 continue
将跳过循环内的其余代码,并立即转到下一个迭代。 break
停止循环并在循环主体之后继续执行。
在代码中使用continue
时,您必须小心。 例如,如果您从上方开始执行do-while
循环,并说要在i
等于5时continue
执行,则可能会导致无限循环,该无限循环会一直运行,具体取决于您放置continue
语句的位置:
do {
print(i);
if (i == 5) {
continue;
}
++i;
} while (i < 10);
// 1
// 2
// 3
// 4
// 5
// 5
// 5
// 5
// 5
// 5
// 5
// 5
// 5
// 5
// ...
由于一旦i
为5
,便会无限循环,您永远不会递增i
,并且条件始终为true
。
如果您在DartPad
中运行此程序,则无限循环将导致浏览器挂起。 而是使用break
,这样循环将在i
达到5
后结束:
do {
print(i);
if (i == 5) {
break;
}
++i;
} while (i < 10);
// 1
// 2
// 3
// 4
// 5
现在运行代码,看看循环在5
次迭代后结束。
4. For Loops
循环预定次数的循环是Dart中的for
循环,再次类似于其他语言中的循环。
Dart具有类似于C的形式的for循环,带有初始化,循环条件和操作,以及for-in
循环,用于迭代对象集合。 在第一种形式中,初始化在第一次循环迭代之前运行。 在进入每个迭代时检查条件,并在开始下一个迭代之前运行操作。
对于for-in
形式,对于循环的每个后续迭代,将一个变量设置为Dart对象集合中的每个元素。 假设您要对前10个整数的值求和。
为总和创建一个变量:
var sum = 0;
然后使用for
循环,在该循环中将循环计数器i
初始化为1,检查i
小于或等于10,并在每个循环后递增i
。 在循环内使用复合赋值将i
加到运行总和中:
for (var i = 1; i <= 10; i++) {
sum += i;
}
print("The sum is $sum"); // The sum is 55
在DartPad
中运行代码以查看总和。
Dart
集合的一个示例是用方括号括起来的简单数字列表:
var numbers = [1, 2, 3, 4];
您可以使用for-in
循环遍历列表:
for (var number in numbers) {
print(number);
}
// 1
// 2
// 3
// 4
在循环中迭代时,可变number
采用numbers
中每个元素的值。
像numbers
这样的列表也有一个名为forEach
的函数,您可以将其调用,从而将前一个循环简化为一行:
numbers.forEach((number) => print(number));
// 1
// 2
// 3
// 4
在这里,您使用了匿名函数(anonymous function)
和箭头语法(arrow syntax)
,稍后将在Functions
部分中看到这两者。
最后,像while循环一样,for循环也可以使用continue
和break
来控制循环内的流。 例如,如果要跳过打印数字3
,则可以在for-in
循环中使用continue
语句:
for (var number in numbers) {
if (number == 3) {
continue;
}
print(number);
}
// 1
// 2
// 4
5. switch and enum
Dart
还支持switch
语句和使用enum
的枚举。 有关这两者的更多信息,请查阅Dart文档Dart documentation。 就像您所见过的大多数其他Dart构造一样,它们的工作方式与在其他语言(如C和Java)中的工作方式类似。
Functions
函数使您可以将多个相关的代码行打包到一个包体中,然后可以将其召唤以避免在整个Dart应用程序中重复这些代码行:
函数由返回类型,函数名称,括号中的参数列表以及括在括号中的函数体组成。 您要变成函数的代码放在花括号内。 调用函数时,您传入的参数与函数的参数类型匹配。
注意:在函数上指定返回类型是可选的。 如果不使用返回类型,则从函数返回的值将具有推断的类型,或者如果无法推断该类型,则为
dynamic
。 这使您可以从函数中返回不同类型的值。 通常,您会希望避免这种情况,而是从函数中指定单个返回类型。
通常,函数是在其他函数外部或Dart类内部定义的,但是您也可以将Dart
函数相互嵌套。 例如,您可以在此部分的main
中添加函数。
在DartPad
中编写一个新函数,该函数将仅检查给定的字符串是否为banana
。
bool isBanana(String fruit) {
return fruit == 'banana';
}
该函数使用return
返回由传递给该函数的参数确定的布尔值。 对于任何给定的输入,此函数将始终返回相同的值。 如果某个函数不需要返回值,则可以将返回类型设置为void
,例如对于main
函数。
您可以通过传入字符串来调用该函数。 然后,您可以将该调用的结果传递给print
:
var fruit = 'apple';
print(isBanana(fruit)); // false
您可以更改传递给函数的参数,然后使用新参数再次调用它们:
fruit = 'banana';
print(isBanana(fruit)); // true
调用函数的结果完全取决于传入的参数。
1. Optional Parameters
如果函数的参数是可选的,则可以用方括号括起来:
String fullName(String first, String last, [String title]) {
return "${title == null ? "" : "$title "}$first $last";
}
如果函数调用中未包含可选参数,则函数体内的参数使用的值为null
。
然后,您可以调用带有或不带有可选参数的函数:
print(fullName("Joe", "Howard"));
// Joe Howard
print(fullName("Albert", "Einstein", "Professor"));
// Professor Albert Einstein
2. Optional Named Arguments
使用Dart函数,可以在参数列表中使用大括号括起来,以使用可选的命名参数(optional named arguments)
:
bool withinTolerance({int value, int min, int max}) {
return min <= value && value <= max;
}
然后,可以通过为参数名称提供冒号来以不同顺序传递参数:
print(withinTolerance(min: 1, max: 10, value: 11)); // false
像可选参数一样,带有可选名称的参数不需要添加到函数调用中,并且相应的参数在函数中将被赋予null in the function
值。
3. Default Values
您还可以使用equals
将默认值分配给一个或多个参数:
bool withinTolerance({int value, int min = 0, int max = 10}) {
return min <= value && value <= max;
}
然后,可以在调用函数时保留默认值的参数。
print(withinTolerance(value: 5)); // true
运行您的代码以查看新函数的实际作用。
4. First-Class Functions
Dart
支持所谓的first-class functions
。 该术语意味着将函数与任何其他数据类型一样对待,也就是说,可以将它们分配给变量,作为参数传递并从其他函数返回。
您可以使用Function
类型来指定名为op
的参数本身就是一个函数:
int applyTo(int value, int Function(int) op) {
return op(value);
}
因此,如果您有一个名为square
的函数:
int square(int n) {
return n * n;
}
您可以传递square
以将applyTo
用作参数:
print(applyTo(3, square)); // 9
您可以将函数分配给变量:
var op = square;
然后,您可以在变量上调用该函数,就好像它是原始函数的别名一样:
print(op(5)); // 25
稍后您将了解有关从其他函数返回函数的更多信息。
5. Anonymous Functions and Closures
到目前为止,您已经看到的功能已被命名为功能,即带有名称的功能。 匿名函数正是您所期望的,没有名称的函数。 您也可以省略将推断出的返回类型。 因此,您只需要一个参数列表和一个用于匿名函数的函数主体。
您可以将匿名函数分配给变量:
到目前为止,您已经看到的函数已被命名为函数,即带有名称的函数。 匿名函数正是您所期望的,没有名称的函数。 您也可以省略将推断出的返回类型。 因此,您只需要一个参数列表和一个用于匿名函数的函数主体。
您可以将匿名函数分配给变量:
var multiply = (int a, int b) {
return a * b;
};
右侧由参数列表和函数主体组成。
由于multiply
现在拥有一个匿名函数,因此您可以像调用其他任何函数一样调用它:
print(multiply(14, 3)); // 42
您之前已经看到了列表和forEach
函数的预览。 forEach
是使用匿名函数的一个很好的例子。
因此,如果您有一个数字列表:
numbers = [1, 2, 3];
您可以在列表上调用forEach
并传入匿名函数,该函数将元素三倍并打印出三倍的值:
numbers.forEach((number) {
var tripled = number * 3;
print(tripled);
// 3
// 6
// 9
});
您可以从另一个函数返回一个匿名函数:
Function applyMultiplier(num multiplier) {
return (num value) {
return value * multiplier;
};
}
这对于生成具有给定行为但输入参数值变化的函数很有用。 因此,这里的返回值是一个函数,该函数采用一个num
参数并将其乘以用于生成该函数的倍数。
一个示例是创建一个名为Triple
的变量,该变量将其输入乘以3:
var triple = applyMultiplier(3);
您可以使用num
的任何一种来调用Triple
:
print(triple(6)); // 18
print(triple(14.0)); // 42
Dart
中的匿名函数充当闭包,这意味着它们在自身外部定义的“闭包”变量。 例如,上面的applyMultiplier
函数的返回值是一个闭包,该闭包可以访问在其他地方定义的multiplier
变量,在这种情况下,位于applyMultiplier
本身的参数列表中。
6. Arrow Syntax
当Dart
函数(命名的或匿名的)仅由一行代码组成时,您可以使用Dart箭头语法(arrow syntax)
来简化函数主体。
例如,上面的multiply
变为:
var multiply = (int a, int b) => a * b;
您已经删除了大括号和return
语句。
您还可以将箭头语法与applyMultiplier
结合使用:
Function applyMultiplier(num multiplier) =>
(num value) => value * multiplier;
您在这里使用了两次箭头语法:第一个是表示applyMultiplier
函数的返回值,第二个是在返回值本身之内,这是一个匿名函数。
在上面使用forEach
的示例中,您不能使用箭头语法,因为您传递给forEach
的函数的主体具有多行代码。
查看Dart官方文档official Dart documentation以了解Dart的中级和高级部分:
Iterables and Generators
Exceptions
Runes and symbols
Asynchronous Dart with isolates, streams, futures and async/await
后记
本篇主要讲述了Dart 入门,感兴趣的给个赞或者关注~~~