本篇主要通過一個簡單例子,討論一下 Dart 程式碼裡一個有趣的現象。
我們都知道 Dart 裡一切都是物件,就連基礎型別 int
、double
、bool
也都是 class
。
當我們對於 int
、 double
這些 class
進行的 +
、-
、*
、 \
等操作時,其實是執行了這個 class
的 operator
操作符的操作, 然後返回了新的 num
物件。
對於這些 operator
操作最終會通過 VM
去進行實現返回,而本質上 dart 程式碼也只是文字,需要最終編譯成二進位制去執行。
以下例子基於 dart 2.12.3 測試
那這裡想要討論什麼呢?
首先我們看一段程式碼,如下程式碼所示,可以看到:
- 首先我們定義了一個叫
idx
的int
型引數; - 然後在
for
迴圈裡新增了三個InkWell
可點選控制元件; - 最後在
onTap
裡面將idx
列印出來;
class MyHomePage extends StatelessWidget {
var images = ["RRR", "RRR", "RRR",];
@override
Widget build(BuildContext context) {
List<Widget> contents = [];
int idx = 0;
for (var imgUrl in images) {
contents.add(InkWell(
onTap: () {
print("######## $idx");
},
child: Container(
height: 100,
width: 100,
color: Colors.red,
child: Text(imgUrl),
)));
idx++;
}
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: [
...contents,
],
)));
}
}
複製程式碼
-
問題來了,你覺得點選這三個
InkWell
列印出來的會是什麼結果? -
答案是列印出來的都是 3。
為什麼呢?讓我們看這段程式碼編譯後的邏輯,如下所示程式碼,可以看到上述程式碼編譯後, print
函式裡指向的永遠是 idx
這個 int*
指標,當我們點選時,最終列印出來的都是最後的 idx
的值。
@#C475
method build(fra2::BuildContext* context) → fra2::Widget* {
core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
core::int* idx = 0;
{
core::Iterator<core::String*>* :sync-for-iterator = this.{main::MyHomePage::images}.{core::Iterable::iterator};
for (; :sync-for-iterator.{core::Iterator::moveNext}(); ) {
core::String* imgUrl = :sync-for-iterator.{core::Iterator::current};
{
[@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}(new ink5::InkWell::•(onTap: () → Null {
core::print("######## ${idx}");
}, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, $creationLocationd_0dea112b090073317d4: #C66610), $creationLocationd_0dea112b090073317d4: #C66614));
idx = idx.{core::num::+}(1);
}
}
}
複製程式碼
那如果我們需要列印出來的是每個 InkWell
自己的 index
呢?
如下程式碼所示,我們在 for 迴圈裡增加了一個 index
引數,把每次 idx
都賦值給 index
,這樣點選列印出來的結果,就會是點選對應的 index
。
class MyHomePage extends StatelessWidget {
var images = ["RRR", "RRR", "RRR",];
@override
Widget build(BuildContext context) {
List<Widget> contents = [];
int idx = 0;
for (var imgUrl in images) {
int index = idx;
contents.add(InkWell(
onTap: () {
print("######## $index");
},
child: Container(
height: 100,
width: 100,
color: Colors.red,
child: Text(imgUrl),
)));
idx++;
}
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: [
...contents,
],
)));
}
}
複製程式碼
為什麼呢?
讓我們看新編譯出來的程式碼,如下所示,可以看到對了 core::int* index = idx;
這段程式碼,然後回憶下前面所說的,Dart 裡基本型別都是物件,而 operator
操作符運算後返回新的物件。
這樣就等於用 index
把每次的操作到儲存下來,而 print
列印的自然就是每次被儲存下來的 idx
。
@#C475
method build(fra2::BuildContext* context) → fra2::Widget* {
core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
core::int* idx = 0;
{
core::Iterator<core::String*>* :sync-for-iterator = this.{main::MyHomePage::images}.{core::Iterable::iterator};
for (; :sync-for-iterator.{core::Iterator::moveNext}(); ) {
core::String* imgUrl = :sync-for-iterator.{core::Iterator::current};
{
core::int* index = idx;
[@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}(new ink5::InkWell::•(onTap: () → Null {
core::print("######## ${index}");
}, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, $creationLocationd_0dea112b090073317d4: #C66610), $creationLocationd_0dea112b090073317d4: #C66614));
idx = idx.{core::num::+}(1);
}
}
}
複製程式碼
那再來個不一樣的寫法。
如下程式碼所示,把 InkWell
放到一個 getItem
函式裡返回,然後 index
通過函式引數傳遞進來,可以看到執行後的結果,也是點選對應 InkWell
列印對應的 index
。
class MyHomePage extends StatelessWidget {
var images = ["RRR", "RRR", "RRR",];
@override
Widget build(BuildContext context) {
List<Widget> contents = [];
int idx = 0;
getItem(int index, String imgUrl) {
return InkWell(
onTap: () {
print("######## $index");
},
child: Container(
height: 100,
width: 100,
color: Colors.red,
child: Text(imgUrl)));
}
for (var imgUrl in images) {
contents.add(getItem(idx, imgUrl));
idx++;
}
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: [
...contents,
],
)));
}
}
複製程式碼
為什麼呢?
我們繼續看編譯後的程式碼,如下程式碼所示,其實就是每次的 idx
都通過 getItem.call(idx)
被 getItem
的 index
引用,然後下次又再次傳遞一個對應的 idx
進去,原理其實和上面的情況一樣,所以每次點選也會列印對應的 index
。
@#C475
method build(fra2::BuildContext* context) → fra2::Widget* {
core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
core::int* idx = 0;
function getItem(core::int* index) → ink5::InkWell* {
return new ink5::InkWell::•(onTap: () → Null {
core::print("######## ${index}");
}, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, $creationLocationd_0dea112b090073317d4: #C66610), $creationLocationd_0dea112b090073317d4: #C66614);
}
{
core::Iterator<core::String*>* :sync-for-iterator = this.{main::MyHomePage::images}.{core::Iterable::iterator};
for (; :sync-for-iterator.{core::Iterator::moveNext}(); ) {
core::String* imgUrl = :sync-for-iterator.{core::Iterator::current};
{
[@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}([@vm.call-site-attributes.metadata=receiverType:library package:flutter/src/material/ink_well.dart::InkWell* Function(dart.core::int*)*] getItem.call(idx));
idx = idx.{core::num::+}(1);
}
}
}
複製程式碼
最後我們再換種寫法。
如下程式碼所示,直接用最基本的 for
迴圈新增 InkWell
並列印 idx
,結果會怎麼樣呢?
class MyHomePage extends StatelessWidget {
var images = [ "RRR","RRR", "RRR"];
@override
Widget build(BuildContext context) {
List<Widget> contents = [];
for (int idx = 0; idx < images.length; idx++) {
contents.add(InkWell(
onTap: () {
print("######## $idx");
},
child: Container(
height: 100,
width: 100,
color: Colors.red,
child: Text(images[idx]),
)));
}
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: [
...contents,
],
)));
}
}
複製程式碼
答案就是:點選對應 InkWell
列印對應的 index
。
為什麼呢?
我們繼續看編譯後的程式碼,可以看到都是列印的 idx
,為什麼這樣就可以正常呢?
這裡最大的不同就是idx
被宣告的位置不同。
@#C475
method build(fra2::BuildContext* context) → fra2::Widget* {
core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
for (core::int* idx = 0; idx.{core::num::<}(this.{main::MyHomePage::images}.{core::List::length}); idx = idx.{core::num::+}(1)) {
[@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}(new ink5::InkWell::•(onTap: () → Null {
core::print("######## ${idx}");
}, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, child: new text::Text::•(this.{main::MyHomePage::images}.{core::List::[]}(idx), $creationLocationd_0dea112b090073317d4: #C66607), $creationLocationd_0dea112b090073317d4: #C66613), $creationLocationd_0dea112b090073317d4: #C66617));
}
複製程式碼
那這時候我們重新調整下,把 idx
放到 for 外面,點選測試會發現,列印的結果又都是 3。
class MyHomePage extends StatelessWidget {
var images = [ "RRR", "RRR","RRR"];
@override
Widget build(BuildContext context) {
List<Widget> contents = [];
int idx = 0;
for (; idx < images.length; idx++) {
contents.add(InkWell(
onTap: () {
print("######## $idx");
},
child: Container(
height: 100,
width: 100,
color: Colors.red,
child: Text(images[idx]),
)));
}
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: [
...contents,
],
)));
}
}
複製程式碼
這是為什麼呢?
看編譯後的程式碼,唯一不同的就是 core::int* idx
的宣告位置,那原因究竟是什麼呢?
@#C475
method build(fra2::BuildContext* context) → fra2::Widget* {
core::List<fra2::Widget*>* contents = core::_GrowableList::•<fra2::Widget*>(0);
core::int* idx = 0;
for (; idx.{core::num::<}(this.{main::MyHomePage::images}.{core::List::length}); idx = idx.{core::num::+}(1)) {
[@vm.call-site-attributes.metadata=receiverType:dart.core::List<library package:flutter/src/widgets/framework.dart::Widget*>*] contents.{core::List::add}(new ink5::InkWell::•(onTap: () → Null {
core::print("######## ${idx}");
}, child: new con7::Container::•(height: 100.0, width: 100.0, color: #C40086, child: new text::Text::•(this.{main::MyHomePage::images}.{core::List::[]}(idx), $creationLocationd_0dea112b090073317d4: #C66607), $creationLocationd_0dea112b090073317d4: #C66613), $creationLocationd_0dea112b090073317d4: #C66617));
}
複製程式碼
因為 onTap
是在點選後才輸出引數的,而對於 for (core::int* idx = 0;
來說,idx
的作用域是在 for
迴圈之內,所以編譯後在 onTap
內要有對應持有一個值,來儲存需要輸出的結果。
而對於 for
迴圈外定義的 core::int* idx
, 迴圈內的所有 onTap
都可以指向它這個地址,所以導致點選時都輸出了同一個 idx
的值。
至於為什麼會有這樣的邏輯,在深入的執行時邏輯就沒有去探索了(懶),推測應該是編譯後的二進位制檔案在執行時,針對迴圈外的引數和迴圈內的引數優化有關係。
理論上,應該是屬於變數捕獲:
- 對於全域性變數,不會捕獲,通過全域性變數訪問。
- 對於區域性變數,自動變數將會捕獲,且是值傳遞。
最後,如果你也想檢視 dill
內容,可以通過 mac 下的 xxd 命令:
xxd /Users/xxxxxxx/workspace/flutter-wrok/flutter_app_test/.dart_tool/flutter_build/bf7ed8e7e7b3e64f28f0af8a89a29ca9/app.dill
複製程式碼
也可以通過 dump_kernel.dart
(在完整版 dart-sdk
的/Users/guoshuyu/workspace/dart-sdk/pkg/vm
目錄下)執行如下命令,生成 app.dill.txt
檢視,比如你可以檢視 final
和 const
編譯後的區別。
dart dump_kernel.dart /Users/xxxxxxx/workspace/flutter-wrok/flutter_app_test/.dart_tool/flutter_build/bf7ed8e7e7b3e64f28f0af8a89a29ca9/app.dill /Users/xxxxxxx/workspace/flutter-wrok/flutter_app_test/.dart_tool/flutter_build/bf7ed8e7e7b3e64f28f0af8a89a29ca9/app.dill.txt
複製程式碼