Dart 语言基础与核心特性

编程语言有很多虽然千差万别,但归根结底,所有的语言的设计思想都要解决两个问题: 1 . 如何表示信息 2 . 如何处理信息

Dart 如何表示信息:基础语法与类型变量

Dart中的基础语法和Java、JavaScript类似,这里不进行详细的介绍了,对于有Java/JavaScript基础的同学,甚至简单了解一下写法就可以上手.Dart不像JavaScript
弱类型的语言,Dart集成了Java/JavaScript的优点,Dart既可以设置强类型也可以设置var 动态识别类型的方式,也实现了函数式的编程.

Dart 是类型安全的,有基本的数据类型int、num、bool、String、List、Map、Object等. var 是一种不指定类型声明变量的方式,但是var一旦确定变量的类型就不能改变.
看如下代码:

main(List<String> args) {
  var number = 100;
  int num2 = 200;
  print('number is var:$number,num2 is int:$num2');
}

和Java/C等编程语言一样,要求以main函数作为执行入口.

Dart的变量

Dart 中,可以使用var定义一个变量,变量表示的类型由编译器推断;也可以使用静态类型定义变量,更清楚的定义变量的类型.
没有初始化的变量自动获取一个默认值为null,这点Dart借鉴了Java中的设计,没有初始化的变量为null值,没有了JavaScript中的undefined、烫烫烫等语句,但是注意Java中int类型的变量默认为0,boolean类型的变量默认为false,在Dart中类型为数字的变量如果没有初始化默认值为null(PS:其实我还是喜欢Java中没有初始化变量默认值的设计,可能是习惯问题),当然这样你必须保证int类型的变量必须有初始化值,以保证你的逻辑严谨.
看如下代码:

var number;
int num2;
num num1;
double num3;
print('number is var:$number,num2 is int:$num2,num2 is num:$num1,num3 is double:$num3');

OUTPUT:number is var:null,num2 is int:null,num2 is num:null,num3 is double:null

可以看出,所有的数字类型的没有初始化默认值都为null包括var.
我们在看一下其他类型的变量,代码如下

bool bool1;
String str;
List list;
Map map;
print('bool1:$bool1,str:$str,list:$list,map:$map');

OUTPUT:bool1:null,str:null,list:null,map:null

可以看到,其他类型的变量如果没有初始化,默认值都是null,这是Dart与其他语言不同的地方.
不知道你有没有注意到一点Dart中并没有float类型,其中int、double都是继承自num.

abstract class int extends num
abstract class double extends num

所有能够使用变量引用的都是对象,每个对象都是一个类的实例.在Dart中数字、方法和null都是对象,所有的对象都继承于Object类.

还有一点注意,Dart是类型安全的,不能像JavaScript中的if(0)或assert(1) 之类的这样的判断,因为在JavaScript中会自动转换为boolean.if或assert中必须是true或false.

var number = 0;
assert(number);//Error 必须是bool类型

assert(number == 0);//正确的 

Dart中也没有===向JavaScript中这样的判断.

Number类型

Dart支持两种数字类型:intdouble它们都是num的子类,num定义了基础操作符+、-、/和 还定义了一些abs() ceil()等函数
下面主要看一下数字和字符串之间的转换方式与其他语言有什么不同.
代码如下: String -> num; num -> String; 和其他语言相比并没有什么不同.

  var one = int.parse('1');//String -> int
  assert(one == 1);
  var one2 = double.parse('1.1');//String -> double 
  assert(one2 == 1.1);
  String one3 = 1.toString();//int -> String
  assert(one3=='1');
  String one4 = 3.14159.toStringAsFixed(2);
  assert(one4 == '3.14');

我们来看Dart是否存在JavaScript中经典的问题:0.1+0.2 = ?

   var text = 0.1 + 0.2;
  // assert(text == 0.3);
  print(text);

  OUTPUT:0.30000000000000004

上述代码可以看出:0.1 + 0.2 不等于0.3,和JavaScript的输出几乎一样.

String 类型

Dart中String类型的设计和JavaScript中的类似,可以使用单引号也可以使用双引号,同时支持在字符串中嵌入变量和表达式,字符串拼接可以使用+或者多个字符串放到一起拼接为一个字符串和多行字符串“”“ “”“''' '''这点借鉴了Python的设计
代码如下:

  var s15 = 'The String' + 'Works even over line breaks';
  var s16 = 'The ' 'concatenation' "hello world";
  print("s15:$s15,s16:$s16");
  OUTPUT:s15:The StringWorks even over line breaks,s16:The concatenationhello world

  var s17 = """ You can create
  multi-line strings like this one.
  """;
  print(s17);
  OUTPUT:You can create
  multi-line strings like this one.

  var s18 = r"In a raw string,even \n isn't special.";//创建原始 raw 字符串
  print(s18);
  OUTPUT:In a raw string,even \n isn't special.

  var s19 = '$number $bool1 $s18';//$表达式也可以使用
  print(s19);

final和const

如果你不打算修改一个变量,使用final和const.一个final变量只能被赋值一次;一个const变量是编译时常量(const变量同时也是final变量,同样也只能被赋值一次)

  final name = 'Bob';
  // name = 'Alice';//final修饰的变量只能被赋值一次
  const bar = 1000;
  // bar = 100;//const 修饰的变量只能被赋值一次

List和Map

定义List类型的变量借鉴了JavaScript的设计,代码如下:

  var arr1 = ['Tom','Andy','Jack'];
  var arr2 = List.of([1,2,3]);
  arr2.add(499);
  arr2.forEach((v) => print('${v}'));
  arr1.forEach((g) => print('$g'));

  OUTPUT:1
2
3
499
Tom
Andy
Jack

定义map类型的变量

  var map1 = {'name':'Tome','sex':'male'};
  var map2 = Map();
  map2['name'] = 'Tom';
  map2['sex'] = 'male';
  map2.forEach((k,v) => print('$k : $v'));

上述代码中,list和map会自动推导类型,后续添加的类型也必须一致,否则编译器会报错,比如上述代码:会被自动推导为List\<int> Map\<String,String> 类型,这点和java语言类似,初始化变量加上类型约束,那么上述代码可以变成:

  var arr1 = <String>['Tom','Andy','Jack'];
  var arr2 = List<int>.of([1,2,3]);
  arr2.add(499);
  arr2.forEach((v) => print('${v}'));
  arr1.forEach((g) => print('$g'));

  var map1 = <String,String>{'name':'Tome','sex':'male'};
  var map2 = Map<String,String>();
  map2['name'] = 'Tom';
  map2['sex'] = 'male';
  map2.forEach((k,v) => print('$k : $v'));

语义更加清晰了.
同样也可以定义一个不可变的list和map对象.

  var constList = const [1,2,3];
  final constMap = const{
    2 : 'helium',
    10 : 'neon',
    18 : 'argon'
  };

Dart的流程控制语句, if and else,for loops,while and do-while loops
,break and continue,switch and case,assert 与其他语言类似这里就不在复述了,更多的可以去官方文档了解流程控制.

OK,以上为Dart的基础语法和类型变量,有了较为初步的印象,如果有编程语言经验就可以快速的上手.

Dart如何处理信息:对象(函数、类和运算符)

上述我们讲解了如何使用Dart来表达信息,下面我们来讲解Dart 是如何处理信息的.

Function(函数)

函数可以理解为独立完成某个功能的代码.在Dart中所有的类型都是对象,包括函数也是对象,那么就意味着函数可以被定义为变量(PS:这一点和Java中不同,Java中函数不能作为变量),也可以被定义为参数传递给另一个函数.

如下代码示例:

bool isZero(int number){
  return number == 0;
}

void printInfo(int number,Function check){
  print('$number is Zero:${check(number)}');
}
printInfo(0, isZero);

输出:0 is Zero:true

同时也可以使用类似ES6的写法用箭头函数来简化这个函数:

bool isZero(int number) => number == 0;
void printInfo(int number,Function check) => 
  print('$number is Zero:${check(number)}');

Dart中,一个函数可能需要传递多个参数,在Java中是提供函数重载,同名不同参数的函数.但是在Dart中不支持重载,Dart认为重载会导致混乱,从而提供了可选命名参数和可选位置参数.

可选命名参数(Optional named parameters): 给参数增加 ,以paramName:value的方式制定调用参数(在实际开发中会经常用到)
可选位置参数(Optional positional parameters):把一些方法的参数放到中就变成可选位置参数了

//定义可选命名参数
void enable1Flags({bool bold,bool hidden}) => print('$bold,$hidden');

//定义可选命名参数,并增加默认值
void enable2Flags({bool bold = true,bool hidden = false}) => print('$bold,$hidden');
//定义可选位置参数
void enable3Flags(bool bold,[bool hidden,bool h2 = false]) => print('$bold,$hidden,$h2');

  enable1Flags(bold: true,hidden: false);//true,false
  enable1Flags(bold: true);//true,null
  enable2Flags(bold: false);//false,false

  enable3Flags(true,false,true);//true,false,true
  enable3Flags(true);//true,null,false
  enable3Flags(true,true);//true,true,false

Classes(类)

Dart 是面向对象的语言,实现类与类方法和变量与Java几乎一致,包括extends(继承某个类) 和 abstract(抽象类) 这里就不在展开一一讲解了,需要注意的是Dart也有Java中的implements接口类的实现,Dart中也没有public、protected、private这些关键字,如果要设置为私有的private只需要在前面加上_就可以,默认都是public.

下面看一个案例,Dart 是如何定义类的.一个类包括最基本的成员变量、构造函数、静态变量、静态函数、其他函数

class Point{
  num x;//成员变量
  num y;
  static num factor =0;//静态变量
  Point(this.x,this.y);//构造函数
  void printInfo() => print('($x,$y)');
  static void printFactor() => print('$factor');//静态方法
}

  var point = Point(100,200);
  point.x = 4;//改变成员变量的值
  point.printInfo();//(4,200)
  Point.factor = 111;//改变静态变量的值
  Point.printFactor();//111

类的实例化提供了多种初始化方式,除了上述讲的可选命名参数和可选位置参数外,此外,Dart还提供了类似kotlin和C plus plus 初始化列表,在构造函数执行之前,给变量赋值,甚至重定向至另一个构造函数.

class Point{
  num x;//成员变量
  num y;
  num z;
  static num factor =0;//静态变量
  Point(this.x,this.y):z = 0;//初始化变量z
  Point.bottom(num x):this(x,0);//重定向构造函数
} 
var point = Point.bottom(100);
point.printInfo();//(100,0,0)

Dart 还支持Factory constructors(工厂方法构造函数)和Constant constructors(常量构造函数)

  • 常量构造函数:如果你的类提供一个状态不变的对象,你可以把这些对象 定义为编译时常量.

    class ImmutablePoint{
      final num x;
      final num y;
      const ImmutablePoint(this.x,this.y);
      static final ImmutablePoint origin = const ImmutablePoint(0, 0);
    }
    
  • 工厂方法构造函数:如果一个构造函数并不总是返回一个新的对象,则使用factory来定义这个构造函数.例如,一个工厂构造函数 可能从缓存中获取一个实例并返回,或者 返回一个子类型的实例(工厂构造函数无法访问this)

class Logger{
  final String name;
  bool mute = false;

  static final Map<String,Logger> _cache = <String,Logger>{};

  factory Logger(String name){
    if(_cache.containsKey(name)){
      return _cache[name];
    }else{
      final logger = Logger._internal(name);
      _cache[name] = logger;
      return logger;
    }
  }

  Logger._internal(this.name);

  void log(String msg){
    if(!mute){
      print(msg);
    }
  }
}

  var logger = new  Logger('UI');
  logger.log('Button clicked');//Button clicked

复用:在任何面向对象的编程语言中,类都有复用的方式,一般有两种方式:继承接口 . 除此之外,Dart还提供了另一种机制来实现类的复用,即“混入”(Minxin).混入鼓励代码重用,可以被视为具有实现方法的接口,这样可以解决Dart缺少多重继承的问题,还能够避免由于多重继承可能导致的歧义.接下来看看Dart是如何实现复用的.

class Ponit1{
  num x = 0,y =0;
  void printInfo() => print('($x,$y)');
}

class Vector extends Ponit1{
  num z = 0;
  
  void printInfo() {
    print('($x,$y,$z)');//覆写父类的实现
  }
}

//Coordinate 对 Point1 的接口实现
class Coordinate implements Ponit1{
  //成员变量需要重新声明
  
  num x;
  
  num y;
 //成员函数需要重新声明实现
  
  void printInfo() {
    print('($x,$y)');
  }
}

  var vector = Vector();
  vector..x = 1..y=2..z=3;//级联运算符,语法糖,相当于vector.x=1 vector.y=2 vector=3
  vector.printInfo();//(1,2,3)

  var coordinate = Coordinate();
  coordinate..x = 1..y=2;
  coordinate.printInfo();//(1,2)

  print(vector is Ponit1);//true
  print(coordinate is Coordinate);//true

上述代码中,子类Coordinate采用接口实现的方式,可以看到Coordinate都要重新声明父类的变量和函数,Dart 的接口实现方式并不像Java的接口实现,Dart的接口实现只是获取到父类的一个“空壳子”,并不能复用Point1的原有实现,为了解决这样的问题,Dart 可以使用混入,通过with关键字来实现

//Coordinate 对 Point1 的接口实现
class Coordinate with Ponit1{

}

Dart 通过混入,可以使一个类通过非继承的方式使用其他类中的变量和方法.

Getters and Setters,Dart 和kotlin类似每个变量都隐含的具有一个getter和setter

class Rectangle{
  num width;

  Rectangle(this.width);

  num get left => left + width;
      set left(num value) => left = width + value;
}

运算符

Dart中的运算符和其他编程语言基本运算符类似,这里不在复述,可以参考文档Dart运算符

Dart 借鉴了C plus plus 的运算符覆写的机制,不仅可以覆写运算符还可以自定义运算符(PS:Dart 集百家之长)

class Vector{
  num x,y = 0;
  
  void printInfo() {
    print('($x,$y)');//覆写父类的实现
  }

  Vector(this.x,this.y);

  //自定义相加运算符
  Vector operator +(Vector v) => Vector(x+v.x,y+v.y);

  bool operator ==(dynamic v) => x==v.x && y == v.y;
}

  final x = Vector(3,3);
  final y = Vector(2,2);
  final z = Vector(1,1);
  print(x==(y+z));//true

实例掌握Dart核心特性

在上述的篇幅中,着重讲解了Dart如何表达信息和处理信息,掌握了Dart的核心特性,下面通过一个实例来进一步了解Dart的设计思想.
^1
通过Dart写一段购物车的程序

///定义商品item类
class Item{
  double price;//商品价格
  String name;//商品名称
  Item(this.price,this.name);
}
///购物车类
class ShoppingCart{
  String name;//用户名
  DateTime date;//日期
  String code;//优惠码
  List<Item> bookings;//购买的商品

  price(){
    double sum = 0.0;
    for (var i in bookings) {
      sum += i.price;
    }
    return sum;
  }

  ShoppingCart(this.name,this.code):date = DateTime.now();

  getInfo(){
    return '购物车信息'
    '\n----------------------------'
    '\n 用户名:$name'
    '\n 优惠码:$code'
    '\n 总价:${price()}'
    '\n 日期:$date'
    '\n----------------------------';
  }
}

var cart = ShoppingCart('张三','123456');
cart.bookings = [Item(10.0, '苹果'),Item(20.0, '香蕉')];
print(cart.getInfo());

上述代码中,定义了商品Item类和购物车ShoppingCart类,购物车的信息通过getInfo方法输出.

购物车信息
----------------------------
 用户名:张三
 优惠码:123456
 总价:30.0
 日期:2019-07-23 18:11:12.705694
----------------------------

上述代码非常简单,结合Dart的一些语法糖基本和Java没什么区别.

下面我们对ItemShoppingCart类进行抽象改造

Item和ShoppingCart类中都有一个name属性和price,从中抽象出一个新的基类Meta

class Meta{
  double price;
  String name;
  Meta(this.price,this.name);
}
class Item extends Meta{
  Item(price,name):super(price,name);
}
class ShoppingCart extends Meta{
  String name;//用户名
  DateTime date;//日期
  String code;//优惠码
  List<Item> bookings;//购买的商品

  double get price{.....}

  ShoppingCart(name,this.code):date = DateTime.now(),super(0,name);

  getInfo(){...}
}

上述代码中,使程序中各个类的依赖关系更加清晰,不过ShoppingCart类中price的get方法和getInfo方法存在着问题.

price属性的get方法,采用了常见的求和算法,一次遍历bookings列表中的Item对象累计相加求和,我们可以重载Item类的‘+’运算符,对列表对象进行归纳合并操作即可.

Item operator+(Item item) => Item(price+item.price, name+item.name);

double get price => bookings.reduce((value,element) => value + element).price;

这样代码就可以简洁很多,再看看getInfo方法如何优化呢? getInfo中进行了多行显示信息,我们可以使用Dart中的多行显示字符串''' '''来进行优化

  getInfo() => '''购物车信息
  ---------------------------
  用户名:$name
  优惠码:$code
  总价:$price
  日期:$date
  ---------------------------
   ''';

去掉了多余的字符串转义和拼接代码,getInfo就更加简单清晰了,也方便以后的修改

对对象参数的优化

  var cart = ShoppingCart('张三','123456');
  cart.bookings = [Item(10.0, '苹果'),Item(20.0, '香蕉')];
  print(cart.getInfo());
  • 期望调用者通过’参数名:键值对’的方式制定调用参数
  • 对一个购物车来说用户名是必须的,优惠码不是必须的
  ShoppingCart({name}):this.withCode(name:name,code:null);

  ShoppingCart.withCode({name,this.code}):date = DateTime.now(),super(0,name);

  var cart = ShoppingCart.withCode(name:'张三',code:'123456');
  cart.bookings = [Item(10.0, '苹果'),Item(20.0, '香蕉')];
  print(cart.getInfo());

 var cart1 = ShoppingCart(name:'李四');
  cart1.bookings = [Item(20.0, '苹果'),Item(30.0, '香蕉')];
  print(cart1.getInfo());

上述代码对ShoppingCart进行了改造, 参数的传递逻辑更加清晰
输出结果:

购物车信息
  ---------------------------
  用户名:张三
  优惠码:123456
  总价:30.0
  日期:2019-07-23 19:03:35.411915
  ---------------------------

购物车信息
  ---------------------------
  用户名:李四
  优惠码: 没有 
  总价:50.0
  日期:2019-07-23 19:03:35.417256
  ---------------------------

关于购物车的打印信息的能力,我希望不去通过main函数获取到购物车信息后,再去使用全局的print函数打印,而是将它作为一个单独是类PrintHelper,那么如何实现PrintHelper的复用呢? 通过混入的方式来实现

abstract class PrintHelper{
  printInfo() => print(getInfo());
  getInfo();
}

class ShoppingCart extends Meta with PrintHelper{
  
  getInfo() => {...}  
}

上述代码,我们直接调用printInfo()就可以打印购物车的信息了. 同时我们还可以使用级联运算符’..’来连续调用多个函数以及成员变量.

  ShoppingCart.withCode(name:'张三',code:'123456')
  ..bookings= [Item(10.0, '苹果'),Item(20.0, '香蕉')]
  ..printInfo();

 ShoppingCart(name:'李四')
 ..bookings = [Item(20.0, '苹果'),Item(30.0, '香蕉')]
 ..printInfo();

OK,到现在我们梳理了Dart的变量语法以及类和函数、运算符,这些都是所有编程语言共有的东西,每学习一个编程语言都可以从这几个方面进行学习,可以很快上手一门编程语言,当然本篇文章只是讲解了基础的部分,另外还有:网络、异步这两个重要的部分.