Dart语法

170 阅读9分钟

Dart.png

一.变量与数据类型

1.变量的声明

1.1.var自动推断

在Dart中,变量的声明有很多中,比如我们可以用var来声明一个类型推断的变量

var name = 'Dart'; // Dart 自动推断出 name 是 String 类型

1.2.指定数据类型

我们在声明的时候就指定其数据类型,比如下面我声明一个字符串类型的数据a

String a = 'Hello Dart'; 

1.3 空与空安全

这个可以参看下koltin的空安全,是在编译时检测空安全,如果我们想定义一个值可能为空,我们需要在变量类型后加上一个?来表示这个变量可能为空,像这样 String? a;,如果不加问号,可能必须要给其赋值,当然我们可以通过late 这个关键字来延时去赋值,另外需要注意的一点,如果我们的变量是可空类型,我们在使用它的使用可以使用变量?.来对其操作,这个代码的意思类 和kotlin一样为空是就不执行后面的操作,同样的对于可空类型的后面也可以加上!!来表示这个值一定不为空,如果为空则会抛异常。

1.4 final与const

  • 相同点

    这两个关键字都用来修饰常量

  • 不同点

    const是编译时必须给具体的数据,但是final可以在运行时给,这点有点像延时初始化的的意思。

 late final String c; //稍后给值
 const String d="";//初始化后就给
  • const的其他用法

1. const 可以用来创建常量值,该常量值可以赋予给任何变量。

 const list1 = [1, 2, 3]; // const关键字用在声明的地方
 var list2 = const [1, 2, 3]; // const关键字也可以用在创建对象的地方
 print(identical(list1, list2)); // 结果为true

所以const对象的值不可变是深度递归的,其内部成员的值也都是不可变的,比如上面的我们取出数组中的第一个,并给其重新赋值,在运行时就会报错

const list1 = [1, 2, 3]; // const关键字用在声明的地方
var list2 = const [1, 2, 3]; // const关键字也可以用在创建对象的地方
print(identical(list1, list2)); // 结果为true
list1[0]=12; //运行时报错 Unsupported operation: Cannot modify an unmodifiable list
print('$list1');

2.const可以将构造函数声明前加上const,表示这种类型的构造函数创建的对象是不可改变的。

注意1:当用const修饰构造方法时(常量构造方法),构造方法的属性一定是final修饰的;

class Person {
  String name; //没有用final修饰就会报下面的错误
 const Person(this.name);
}
   ///Can't define a const constructor for a class with non-final fields. (Documentation)  Try making all of the fields final, or removing the keyword 'const' from the constructor

注意2:当用const定义对象变量时,其构造方法也必须是const修饰;

{
  const p1= Person("name");//不可以new
  const p2=Person("name");//不可以new
  print(identical(p1, p2));

}

class Person {
   final String name;
   const Person(this.name); ///我们在定义p1的时候使用const关键字,但是这个构造方法却没有用const修饰就会报下面的错误:The constructor being called isn't a const constructor.
}

注意3:如果实例化时不加const修饰符,即使调用的是常量构造函数,实例化的对象也不是常量实例;(相同的常量只创建一个)

void main() {
//不是同一个
var p1 = Person("name");
var p2 = Person("name");
print(identical(p1, p2));

//不是同一个
var p3 = new Person("name");
var p4 = new Person("name");
print(identical(p3, p4));

//同一个
const p5 = Person("name");
const p6 = Person("name");
print(identical(p5, p6));

//同一个
var p7 = const Person("name");
var p8 = const Person("name");
print(identical(p7, p8));
}

class Person {
final String name;
const Person(this.name);
}
false
false
true
true

2. 数据类型

1.number类型

int age = 18;
double score = 93.5;

int 和double都是num的子类,如果想定义一个既是double又是int,就可以用num定义

num x = 1;
x += 1.5;
print(x);

2.Strings类型

1.字符串可以用单/双引号,在双引号中使用单引号可以不用转义,反过来也是一样

var s1 = '使用单引号创建字符串字面量。';
var s2 = "双引号也可以用于创建字符串字面量。";
var s3 = '使用单引号创建字符串时可以使用斜杠来转义那些与单引号冲突的字符串:'';
var s4 = "而在双引号中则不需要使用转义与单引号冲突的字符串:'";

2.使用三个单引号或者三个双引号能创建多行字符串

  var s6 = '''
You can create
multi-line strings like this one.
''';
 var s7 = """This is also a
multi-line string.""";

3.如果希望字符串中的内容不会被做任何处理(比如转义),则可以在字符串前面加上 r 来创建 raw 字符串

var s8 = r'在 raw 字符串中,转义字符串 \n 会直接输出 “\n” 而不是转义为换行。';
print('s8:$s8');

3.bool类型

bool isTrue = true;
bool isFalse = false;

4.map、list与set

map是键值对,list是有序可重复,set是无序不可重复...其他的好像没什么好说的

5.Object与dynamic

Object 是所有类的基类,相当于一个可以兼容所有类型的超级类型。dynamic 就是一个动态类,类似 TypeScript 的 any。dynamic就表示这是动态类型,可以绕过编译检查

二.函数的定义与调用

1.常规写法

返回类型(可不写) 函数名(){ 函数体 返回值(如果有) }

2.箭头函数

只有一个表达式的函数能够使用箭头函数简化

String getName() => 'hello dart';

3.函数的参数

3.1 可选参数

参数可选的函数:使用[]表示可选的位置参数,注意如果一个参数可选,但是不能为null,需给个默认值,是否可空,用变量类型+?表示

void fun3([String? str, String str2 = 'default value']) {
  assert(str == null);
  assert(str2 == 'default value');
}

3.2 命名函数

命名参数默认都为可选参数。如果是必要参数,则需要用required 定义函数时,使用{参数 1,参数 2}来指定命名参数

String fun4({required String name, int? age = 10}) => '$name$age';

3.3 匿名函数

匿名函数被当做参数使用

void fun5() {
  const list = ['apples', 'bananas', 'oranges'];
  list.forEach((item) {
    print('${list.indexOf(item)}: $item');
  });
}

上面forEach中的函数参数还可用箭头函数替代

void fun6() {
  const list = ['apples', 'bananas', 'oranges'];
  list.forEach((item) => print('${list.indexOf(item)}: $item'));
}

4.闭包

闭包可以定义为一个函数对象,即使其函数对象的调用在它原始范围之外,也能够访问在它词法范围内的变量。换句话说,闭包是一个能够读取其他函数内部变量的函数。

Function makeAdder(int addBy) {
  return (int i) => addBy + i;
}

void main() {
  var adder = makeAdder(2);
  print(adder(3)); // 输出5
}

三.面向对象

Dart是具有minxi继承的面向对象的语言,这是一种设计和结构化代码的方式,允许我们创建复杂的应用程序,使用基本代码构建并易于理解和维护。

1.类的构造方法

1.1正常写法

///常规写法1这种写法属性为空或给初始值或者late延迟初始化
//late String name;
//int? age;
// Person(String name, int age){
//   this.name = name;
//   this.age=age;
// }

///常规写法2这种简化构造函数的参数可以为空或不为空
Person(this.name, this.age) 

1.2 构造方法后给属性赋值

 /// 构造方法后给默认值的
Person():name="abc",age=13;

1.3 命名构造方法

/// 3.命名式构造函数——使用初始化列表
Person.create(n,a):name=n,age=a;

1.4 私有构造方法

Person._create(n,a):name=n,age=a;

1.5常量构造函数

这个在前面说const关键字中已提到

1.6工厂构造函数

工厂构造函数 Dart中的工厂构造函数(Factory Constructor)是一种特殊类型的构造函数,它可以返回对象的实例,而不一定是类的实例。工厂构造函数通常用于创建复杂对象或在创建对象时执行额外的逻辑。

static final Map<String, Person> _cache = {};
factory Person(String name) {
  if (_cache.containsKey(name)) {
    return _cache[name]!;
  } else {
    final logger = Person._internal(name);
    _cache[name] = logger;
    return logger;
  }
}

Person._internal(this.name);

2.get与set函数

实例属性如果没有初始化的话,默认是 null。所有实例变量都会隐式声明 Getter 方法。可以修改的实例变量和 late final声明但是没有初始化的变量还会隐式声明一个 Setter 方法, 我们可以通过 getter 和 setter 读取或设置实例对象。注意,如果late final 修饰的属性如果赋值了,不可以重新赋值,不然会报错

3.继承与多态

java差不多

4.接口

dart 中没有 interface,我们使用抽象类来定义接口,使用implements来让类匹配接口,正常的话, 如果需要有共同的方法复用,我们用 extends继承,如果需要一个规范约束,那就使用 implements实现

5.Mixins

Mixins 是一种在 Dart 中实现代码重用和组合的方式。通过使用 mixins,我们可以将一个或多个类的功能合并到一个类中,以便可以复用这些功能,而无需继承类。注意:1.mixin修饰的一定是继承自object的; 2.被mixin的类不可以有构造方法; 3. 使用 on 关键字可以指定哪些类可以使用该 Mixin 类


mixin CanFly {
  void fly() {
    print('Flying...');
  }
}


mixin CanSwim {
  void swim() {
    print('Swimming...');
  }
}

class Duck with CanFly, CanSwim {
  void quack() {
    print('Quack!');
  }
}

void main() {
  final duck = Duck();
  duck.fly();  // 输出 Flying...
  duck.swim();  // 输出 Swimming...
  duck.quack();  // 输出 Quack!
}



class A1 {
  void getA() {}
}

mixin B1 on A1 { //on这个关键字用来mixin限定的,指定A1这个类的及其子类才可以mixi B1
  void getB1() {}
}

// class C with B {}     这样写是报错的
class C1 extends A1 with B1 {} 这样就可以了

四.异常处理与泛型

1.异常处理

异常处理主要看几个关键字 try on(捕获指定异常) catch finally

2.泛型

Dart也泛型类和泛型方法、并且有上下限的概念

五.Dart异步编程编程

1.理解事件处理机制

image.png

Dart属于单线程模型,在Dart中维护了两个队列:Event Queue和MircoTaskQueue,Dart会对这两个队列不断循环执行,直到所有任务都执行完。注意:1.添加队列、执行队列是有序的;2.微任务执行优先级大于Event Queue;

Flutter中的EventLooper:I/O、timer、点击、渲染、Isolate之间的message以及Future中的部分代码 而微任务一般是Isolate内和future的部分代码执行,并且微任务非常少。这是因为如果微任务非常多,就会造成事件队列排不上队,会阻塞任务队列的执行(比如用户点击没有反应的情况)。

2.Futrue和Stream的使用

在 Dart 中,我们使用 Future 和 async/await 来进行异步编程。当你调用一个异步函数时,它将立即返回一个 Future 对象。当异步操作完成时,Future 将被“完成”或“解析”

2.1 Future的使用

2.1.1.借助async关键字创建future;
Future<String> apiLogin() async {
  return Future.delayed(const Duration(seconds: 2)).then((value) {
    print('login over');
    return "success";
  });
}

login() async{
  print('loigin start');
  var result =await apiLogin();
  print('login end result:$result');
}
void main(){
  print('main start');
  login();
  print('main end');

}
运行结果:main start
loigin start
main end
login over
login end result:success
2.1.2.Future静态方法创建
 Future.syn//同步执行
 Future.delay //延时
 Future.value 
 Future.wait[]   //等待所有都执行结束
 Futurn.timeout //超时
 Future.any[] //谁先执行完成返回谁
 Future.microtask //向 「微任务队列」中插入一个任务,这样就会提高他执行的效率
 Future.catchError //如果异步任务发生错误,我们可以在catchError中捕获错误
 Future.whenComplete
 //无论异步任务执行成功或失败都需要做一些事的场景,
比如在网络请求前弹出加载对话框,在请求结束后关闭对话框。这种场景,有两种方法,
第一种是分别在then或catch中关闭一下对话框,
第二种就是使用Future的whenComplete回调
未完成状态(uncompleted) 完成状态(completed)
完成状态(completed)
2.1.3.future的执行

Future的代码是加入到事件队列还是微任务队列呢?

Future中通常有两个函数执行体:

1.Future构造函数传入的函数体 2.then的函数体(catchError等同看待) 那么它们是加入到什么队列中的呢?

  • Future构造函数传入的函数体放在事件队列中;
  • then的函数体要分成三种情况:
  • 情况一:Future没有执行完成(有任务需要执行),那么then会直接被添加到Future的函数执行体后;
  • 情况二:如果Future执行完后就then,该then的函数体被放到如微任务队列,当前Future执行完后执行微任务队列;
  • 情况三:如果Future是链式调用,意味着then未执行完,下一个then不会执行;

2.2 Stream的使用

Stream 是 Dart 中处理连续的异步事件的工具。例如,你可以使用 Stream 来读取文件的内容,或者监听用户的鼠标点击。

2.2.1.stream的创建
 Stream.empty:创建一个不产生任何事件的 StreamStream.error:创建一个只产生一个错误事件的 StreamStream.periodic:创建一个周期性地产生事件的 Stream。
 StreamController:手动控制 Stream 的事件和错误

image.png

Stream.fromIterable:
Stream.empty:创建一个不产生任何事件的 StreamStream.error:创建一个只产生一个错误事件的 StreamStream.periodic:创建一个周期性地产生事件的 Stream;
StreamController:手动控制 Stream 的事件和错误


///在这个示例中,我们使用 Stream.fromIterable 创建了一个 Stream,
///它将连续地产生 1 到 5 这五个数字。然后我们使用 await for 循环来监听 Stream 的事件

testStream1() async{
  var stream = Stream.fromIterable([1, 2, 3, 4, 5]);

  await for (var number in stream) {
    print(number);  // 输出:1, 2, 3, 4, 5
  }
}

除了以上方法去遍历stream的结果,我们还可以通过lister方式处理stream的结果


var stream = Stream.fromIterable([1, 2, 3, 4, 5]);

stream.listen(
        (event) {
      print('Received event: $event');
    },
    onError: (error) {
  print('Received error: $error');
},
onDone: () {
print('All done');
});

当然我们也可以通过controller手动控制事件和stream的结果

///我们首先创建了一个 StreamController。然后我们使用 sink.add 方法添加了三个事件,
///使用 sink.addError 方法添加了一个错误。最后我们使用 controller.close 方法表示我们不会再添加任何事件或错误

var controller = StreamController<int>();
controller.sink.add(1);
controller.sink.add(2);
controller.sink.addError('Oops!');
controller.sink.add(3);
controller.close();

await for (var event in controller.stream) {
  print(event);
}

2.2.2.stream的变换
///我们首先使用 where 方法创建了一个只包含偶数的 Stream,然后我们使用 map 方法将每个偶数乘以 2。
testStream5() async {
var stream = Stream.fromIterable([1, 2, 3, 4, 5]);

var evenStream =
    stream.where((event) => event % 2 == 0).map((event) => event * 2);

await for (var event in evenStream) {
  print(event); // 输出:4, 8
}
}
2.2.3.stream的应用场景
  • 用户界面交互

在 Flutter 等 Dart 构建的应用程序中,Stream 可以用来监听并响应用户的交互行为。例如,你可以创建一个自定义的 StreamController,并使用它来监听按钮点击事件:

// 创建一个 StreamController
StreamController controller = StreamController();

void main() {
  // 按钮点击事件监听
  controller.stream.listen((data) {
    print("Button clicked: $data");
  });

  // 模拟按钮点击
  controller.sink.add('Button 1');
}

// 在你的 UI 中,当按钮被点击时,你可以调用 controller.sink.add 来发送一个事件。
  • 网络请求

在进行网络请求时,服务器的响应通常会分成多个数据包。你可以使用 Stream 来连续地接收和处理这些数据包,这样你就可以在不等待整个响应完成的情况下开始处理数据:

import 'dart:convert';
import 'dart:io';

void main() async {
  var client = HttpClient();

  client.getUrl(Uri.parse('https://api.github.com/users/dart-lang/repos'))
    .then((HttpClientRequest request) {
      return request.close();
    })
    .then((HttpClientResponse response) {
      response.transform(Utf8Decoder()).listen((contents) {
        print(contents);
      });
    });
}
  • 文件操作

当你需要读取一个大文件时,可以使用 Stream 来逐行处理文件内容,这样你可以在不需要将整个文件加载到内存的情况下开始处理数据:

import 'dart:convert';
import 'dart:io';

void main() {
  File file = new File('path_to_your_file');
  Stream<List<int>> inputStream = file.openRead();

  inputStream
    .transform(utf8.decoder)       // Decode bytes to UTF-8.
    .transform(new LineSplitter()) // Convert stream to individual lines.
    .listen((String line) {        // Process results.
        print('$line: ${line.length} bytes');
    },
    onDone: () { print('File is now closed.'); },
    onError: (e) { print(e.toString()); });
}
  • 定时任务 你可以使用 Stream 创建一个定时任务,然后在每个时间间隔中执行一些操作。例如,下面的代码使用 Stream.periodic 创建了一个每秒执行一次的定时任务:
void main() {
  // 创建一个每秒触发一次的 Stream
  Stream.periodic(Duration(seconds: 1), (count) => count).listen((count) {
    print('Tick: $count');
  });
}

  • 数据流处理

在处理大量数据流时,你可以使用 Stream 创建一个数据管道,并利用其提供的 mapfilterreduce 等操作进行数据处理。

void main() {
 Stream.fromIterable([1, 2, 3, 4, 5])
   .map((value)

=> value * 2)
   .listen((value) {
     print(value); // 输出 2, 4, 6, 8, 10
   });
}

2.3 Future和Stream的区别
  1. Future 表示稍后获得的一个数据,所有异步的操作的返回值都用 Future 来表示。

  2. Future 只能表示一次异步获得的数据。

  3. Stream 表示多次异步获得的数据。比如界面上的按钮可能会被用户点击多次,按钮上的点击事件(onClick)就是一个 Stream 。

  4. Future将返回一个值,而Stream将返回多次值。

  5. Dart 中统一使用 Stream 处理异步事件流。

3.理解Isolate

Isolate 是 Dart 中进行并发编程的一种方式。由于 Dart 是单线程模型,因此在需要处理 CPU 密集型任务或需要执行长时间运行的操作时,可以使用 Isolate。并且 Dart 中的线程是以隔离 (Isolate)的方式存在的,每个 Isolate 都有自己的 Event Loop 与 Queue, Isolate 之间不共享任何资源,只能依靠消息机制通信,不需要竞争资源,就不需要锁(不用担心死锁问题),因此也就没有资源抢占问题, 每个Isolate 都有自己独立的,私有内存块(多个线程不共享内存)

image.png

3.1 Isolate的创建


import 'dart:isolate';

void printMessage(var message) {
  print('Message: $message');
}

void main() async {
  var receivePort = ReceivePort();
  await Isolate.spawn(printMessage, 'Hello!', onExit: receivePort.sendPort);
  await for (var message in receivePort) {
    print('Received: $message');
  }
}

在这个示例中,我们使用 Isolate.spawn 创建了一个新的 Isolate。我们传递了一个函数 printMessage 和一个消息 'Hello!' 给这个新的 Isolate。当这个新的 Isolate 完成后,它将使用 onExit 参数指定的 SendPort 发送一个消息。

需要注意的是,不同的 Isolate 之间不能共享内存,它们只能通过消息传递来进行通信。因此,你不能在一个 Isolate 中访问另一个 Isolate 的变量。

3.2 Isolate消息的传递

在 Dart 中,Isolate 之间的消息传递是通过 SendPort 和 ReceivePort 来实现的。

void printMessage(var message) {
  var sendPort = message[0] as SendPort;
  var message = message[1] as String;
  
  print('Message: $message');

  sendPort.send('Hello from new Isolate!');
}

void main() async {
  var receivePort = ReceivePort();
  await Isolate.spawn(printMessage, [receivePort.sendPort, 'Hello!']);

  receivePort.listen((message) {
    print('Received: $message');
  });
}

3.3 Isolate的应用场景

3.3.1 数据处理

对于大量的数据处理或复杂的计算任务,例如图像处理、大文件的读写、大数据集合的排序和筛选等,你可以使用 Isolate 进行处理,防止这些操作阻塞 UI 线程,造成应用程序的卡顿或无响应

import 'dart:isolate';

void longRunningTask(SendPort port) {
  // 做一些耗时的操作,例如处理大量数据
  for (int i = 0; i < 1000000000; i++) {}
  port.send("Task done");
}

void main() {
  var receivePort = ReceivePort();
  Isolate.spawn(longRunningTask, receivePort.sendPort);
  receivePort.listen((message) {
    print(message);
  });
}

3.3.2 网络请求

尽管 Dart 的 I/O 操作是非阻塞的,但是在进行网络请求并接收数据时,如果数据量较大或需要复杂的处理(如 JSON 或 XML 的解析),这可能会消耗大量的 CPU 时间,从而阻塞 UI 线程。在这种情况下,你可以使用 Isolate。

void fetchData(SendPort sendPort) async {
  HttpClient httpClient = HttpClient();
  HttpClientRequest request = await httpClient.getUrl(Uri.parse("http://example.com"));
  HttpClientResponse response = await request.close();
  sendPort.send(await consolidateHttpClientResponseBytes(response));
}

void main() async {
  ReceivePort receivePort = ReceivePort();
  await Isolate.spawn(fetchData, receivePort.sendPort);
  List<int> data = await receivePort.first;
  String result = utf8.decode(data);
  print(result);
}

3.3.3 Web 服务器

在编写 Web 服务器时,你可以使用 Isolate 来处理并发的请求。每当收到新的请求时,你可以创建一个新的 Isolate 来处理请求,这样可以避免阻塞服务器的主线程。