Dart語法篇之函式的使用(四)

mikyou發表於2019-11-08

簡述:

在上一篇文章中我們詳細地研究了一下集合有關內容,包括集合的操作符的使用甚至我們還深入到原始碼實現原理,從原理上掌握集合的使用。那麼這篇文章來研究一下Dart的另一個重要語法: 函式

這篇主要會涉及到: 函式命名引數、可選引數、引數預設、閉包函式、箭頭函式以及函式作為物件使用。

一、函式引數

在Dart函式引數是一個比較重要的概念,此外它涉及到概念的種類比較多,比如位置引數、命名引數、可選位置引數、可選命名引數等等。函式總是有一個所謂形參列表,雖然這個引數列表可能為空,比如getter函式就是沒有引數列表的. 此外在Dart中函式引數大致可分為兩種: 位置引數和命名引數,來一張圖理清它們的概念關係

Dart語法篇之函式的使用(四)

1、位置引數

位置引數可以必需的也可以是可選

  • 無引數
//無引數型別-這是不帶函式引數或者說引數列表為空
String getDefaultErrorMsg() => 'Unknown Error!';
//無引數型別-等價於上面函式形式,同樣是引數列表為空
get getDefaultErrorMsg => 'Unknown Error!';
複製程式碼
  • 必需位置引數
//必需位置引數型別-這裡的exception是必需的位置引數
String getErrorMsg(Exception exception) => exception.toString();
複製程式碼
  • 可選位置引數
//注意: 可選位置引數是中括號括起來表示,例如[String error]
String getErrorMsg([String error]) => error ?? 'Unknown Error!';
複製程式碼
  • 必需位置引數和可選位置引數混合
//注意: 可選位置引數必須在必需位置引數的後面
String getErrorMsg(Exception exception, [String extraInfo]) => '${exception.toString()}---$extraInfo';
複製程式碼

2、命名引數

命名引數始終是可選引數。為什麼是命名引數,這是因為在呼叫函式時可以任意指定引數名來傳參。

  • 可選命名引數
//注意: 可選命名引數是大括號括起來表示,例如{num a, num b, num c, num d}
num add({num a, num b, num c, num d}) {
   return a + b + c + d;
}
//呼叫
main() {
   print(add(d: 4, b: 3, a: 2, c: 1));//這裡的命名引數就是可以任意順序指定引數名傳值,例如d: 4, b: 3, a: 2, c: 1
}
複製程式碼
  • 必需位置引數和可選命名引數混合
//注意: 可選命名引數必須在必需位置引數的後面
num add(num a, num b, {num c, num d}) {
   return a + b + c + d;
}
//呼叫
main() {
   print(add(4, 5, d: 3, c: 1));//這裡的命名引數就是可以任意順序指定引數名傳值,例如d: 3, c: 1,但是必需引數必須按照順序傳參。
}
複製程式碼
  • 注意: 可選位置引數和可選命名引數不能混合在一起使用,因為可選引數列表只能位於整個函式形參列表的最後。
void add7([num a, num b], {num c, num d}) {//非法宣告,想想也沒有必要兩者一起混合使用場景。所以
   ...
}
複製程式碼

3、關於可選位置引數[num a, num b]和可選命名引數{num a, num b}使用場景

可能問題來了,啥時候使用可選位置引數,啥時候使用可選命名引數呢?

這裡給個建議: 首先,引數是非必需的也就是可選的,如果可選引數個數只有一個建議直接使用可選位置引數[num a, num b];如果可選引數個數是多個的話建議用可選命名引數{num a, num b}. 因為多個引數可選,指定引數名傳參對整體程式碼可讀性有一定的增強。

4、引數預設值(針對可選引數)

首先,需要明確一點,引數預設值只針對可選引數才能新增的。可以使用 = 來定義命名和位置引數的預設值。預設值必須是編譯時常量。如果沒有提供預設值,則預設值為null

  • 可選位置引數預設值
num add(num a, num b, num c, [num d = 5]}) {//使用=來賦值預設值
    return a + b + c + d;
}
main() {
    print(add(1, 2, 3));//有預設值引數可以省略不傳 實際上求和結果是: 1 + 2 + 3 + 5(預設值)
    print(add(1, 2, 3, 4));//有預設值引數指定傳入4,會覆蓋預設值,所以求和結果是: 1 + 2 + 3 + 4
}
複製程式碼
  • 可選命名引數預設值
num add({num a, num b, num c = 3, num d = 4}) {
    return a + b + c + d;
}
main() {
    print(add(100, 100, d: 100, c: 100));    
}
複製程式碼

二、匿名函式(閉包,lambda)

在Dart中可以建立一個沒有函式名稱的函式,這種函式稱為匿名函式,或者lambda函式或者閉包函式。但是和其他函式一樣,它也有形參列表,可以有可選引數。

(num x) => x;//沒有函式名,有必需的位置引數x
(num x) {return x;}//等價於上面形式
(int x, [int step]) => x + step;//沒有函式名,有可選的位置引數step
(int x, {int step1, int step2}) => x + step1 + step2;////沒有函式名,有可選的命名引數step1、step2
複製程式碼

閉包在dart中的應用

閉包函式在dart用的特別多,單從集合中操作符來說就有很多。

main() {
  List<int> numbers = [3, 1, 2, 7, 12, 2, 4];
  //reduce函式實現累加,reduce函式中接收的(prev, curr) => prev + curr就是一個閉包
  print(numbers.reduce((prev, curr) => prev + curr));
  //還可以不用閉包形式來寫,但是這並不是一個好的方案,不建議下面這樣使用。
  plus(prev, curr) => prev + curr;
  print(numbers.reduce(plus));
}
//reduce函式定義
 E reduce(E combine(E value, E element)) {//combine閉包函式
    Iterator<E> iterator = this.iterator;
    if (!iterator.moveNext()) {
      throw IterableElementError.noElement();
    }
    E value = iterator.current;
    while (iterator.moveNext()) {
      value = combine(value, iterator.current);//執行combine函式
    }
    return value;
  }
複製程式碼

三、箭頭函式

在Dart中還有一種函式的簡寫形式,那就是箭頭函式。箭頭函式是隻能包含一行表示式的函式,會注意到它沒有花括號,而是帶有箭頭的。箭頭函式更有助於程式碼的可讀性,類似於Kotlin或Java中的lambda表示式->的寫法。

main() {
  List<int> numbers = [3, 1, 2, 7, 12, 2, 4];
  print(numbers.reduce((prev, curr) {//閉包簡寫形式
        return prev + curr;
  }));
  print(numbers.reduce((prev, curr) => prev + curr)); //等價於上述形式,箭頭函式簡寫形式
}
複製程式碼

四、區域性函式

在Dart中還有一種可以直接定義在函式體內部的函式,可以把稱為區域性函式或者內嵌函式。我們知道函式宣告可以出現頂層,比如常見的main函式等等。區域性函式的好處就是從作用域角度來看,它可以訪問外部函式變數,並且還能避免引入一個額外的外部函式,使得整個函式功能職責統一。

//定義外部函式fibonacci
int fibonacci(int n) {
    //定義區域性函式lastTwo
    List<int> lastTwo(int n) {
        if(n < 1) {
           return <int>[0, 1];  
        } else {
           var p = lastTwo(n - 1);
           return <int>[p[1], p[0] + p[1]];
        }
    }
    return lastTwo(n)[1];
}
複製程式碼

五、頂層函式和靜態函式

在Dart中有一種特別的函式,我們知道在面嚮物件語言中比如Java,並不能直接定義一個函式的,而是需要定義一個類,然後在類中定義函式。但是在Dart中可以不用在類中定義函式,而是直接基於dart檔案頂層定義函式,這種函式我們一般稱為頂層函式。最常見就是main函式了。而靜態函式就和Java中類似,依然使用static關鍵字來宣告,然後必須是定義在類的內部的。

//頂層函式,不定義在類的內部
main() {
  print('hello dart');
}

class Number {
    static int getValue() => 100;//static修飾定義在類的內部。
}
複製程式碼

六、main函式

每個應用程式都有一個頂級的main()函式,它作為應用程式的入口點。main()函式返回void,所以在dart可以直接省略void,並有一個可選的列表引數作為引數。

//你一般看到的main是這樣的
main() {
  print('hello dart');
}
//實際上它和Java類似可以帶個引數列表
main(List<String> args) {
  print('hello dart: ${args[0]}, ${args[1]}');//用dart command執行的時候: dart test.dart arg0 arg1 =>輸出:hello dart: arg0, arg1    
}
複製程式碼

七、Function函式物件

在Dart中一切都是物件,函式也不例外,函式可以作為一個引數傳遞。其中Function類是代表所有函式的公共頂層介面抽象類。Function類中並沒有宣告任何例項方法。但是它有一個非常重要的靜態類函式apply. 該函式接收一個Function物件function,一個List的引數positionalArguments以及一個可選引數Map<Symbol, dynamic>型別的namedArguments。大家似乎明白了什麼?知道為啥dart中函式支援位置引數和命名引數嗎? 沒錯就是它們兩個引數功勞。實際上,apply()函式提供一種使用動態確定的引數列表來呼叫函式的機制,通過它我們就能處理在編譯時引數列表不確定的情況

abstract class Function {
  external static apply(Function function, List positionalArguments,
      [Map<Symbol, dynamic> namedArguments]);//可以看到這是external宣告,我們需要找到對應的function_patch.dart實現

  int get hashCode;

  bool operator ==(Object other);
}
複製程式碼

在sdk原始碼中找到sdk/lib/_internal/vm/lib/function_patch.dart對應的function_patch的實現

@patch
class Function {
  // TODO(regis): Pass type arguments to generic functions. Wait for API spec.
  //可以看到內部私有的_apply函式,最終接收兩個List原生型別的引數arguments,names分別代表著我們使用函式時
  //定義的所有引數List集合arguments(包括位置引數和命名引數)以及命名引數名List集合names,不過它是委託到native層的Function_apply C++函式實現的。
  static _apply(List arguments, List names) native "Function_apply";

  @patch
  static apply(Function function, List positionalArguments,
      [Map<Symbol, dynamic> namedArguments]) {
    //計算外部函式位置引數的個數  
    int numPositionalArguments = 1 + // 預設同時會傳入function引數,所以預設+1
        (positionalArguments != null ? positionalArguments.length : 0);//位置引數的集合不為空就返回集合長度否則返回0
    //計算外部函式命名引數的個數    
    int numNamedArguments = namedArguments != null ? namedArguments.length : 0;;//命名引數的集合不為空就返回集合長度否則返回0
    //計算所有引數個數總和: 位置引數個數 + 命名引數個數
    int numArguments = numPositionalArguments + numNamedArguments;
    //建立一個定長為所有引數個數大小的List集合arguments
    List arguments = new List(numArguments);
    //集合第一個元素預設是傳入的function物件
    arguments[0] = function;
    //然後從1的位置開始插入所有的位置引數到arguments引數列表中
    arguments.setRange(1, numPositionalArguments, positionalArguments);
    //然後再建立一個定長為命名引數長度的List集合
    List names = new List(numNamedArguments);
    int argumentIndex = numPositionalArguments;
    int nameIndex = 0;
    //遍歷命名引數Map集合
    if (numNamedArguments > 0) {
      namedArguments.forEach((name, value) {
        arguments[argumentIndex++] = value;//把命名引數物件繼續插入到arguments集合中
        names[nameIndex++] = internal.Symbol.getName(name);//並把對應的引數名標識存入names集合中
      });
    }
    return _apply(arguments, names);//最後呼叫_apply函式傳入所有引數物件集合以及命名引數名稱集合
  }
}
複製程式碼

不妨再來瞅瞅C++層中的Function_apply的實現

DEFINE_NATIVE_ENTRY(Function_apply, 0, 2) {
  const int kTypeArgsLen = 0;  // TODO(regis): Add support for generic function.
  const Array& fun_arguments =
      Array::CheckedHandle(zone, arguments->NativeArgAt(0));//獲取函式的所有引數物件陣列 fun_arguments
  const Array& fun_arg_names =
      Array::CheckedHandle(zone, arguments->NativeArgAt(1));//獲取函式的命名引數引數名陣列 fun_arg_names
  const Array& fun_args_desc = Array::Handle(
      zone, ArgumentsDescriptor::New(kTypeArgsLen, fun_arguments.Length(),
                                     fun_arg_names));//利用 fun_arg_names生成對應命名引數描述符集合
 //注意: 這裡會呼叫DartEntry中的InvokeClosure函式,傳入了所有引數物件陣列 fun_arguments和fun_arg_names生成對應命名引數描述符集合
//最後返回result
  const Object& result = Object::Handle(
      zone, DartEntry::InvokeClosure(fun_arguments, fun_args_desc));
  if (result.IsError()) {
    Exceptions::PropagateError(Error::Cast(result));
  }
  return result.raw();
}
複製程式碼

總結

到這裡有關Dart中的函式就說完,有了這篇文章相信大家對dart函式應該有個全面的瞭解了。歡迎持續,下一篇我們將進入Dart中的物件導向...

Dart系列文章,歡迎檢視:

相關文章