自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

軟件開發(fā)探索之道:讓自己成為知識的所有者

開發(fā) 前端
本文我們通過一個 Rust 的例子來探討這個方法。當這個方法本身跟 Rust 無關。我們在學習編程語言,使用第三方庫,構建復雜的系統(tǒng),都可以用這個方法。

 [[422065]]

在做軟件開發(fā)的時候,總會有一些奇奇怪怪的問題難以解答:

  • 棧是向上增長還是向下增長?(這其實是個不嚴謹的問題)
  • arm 是 little endian 還是 big endian?
  • 閉包究竟是一個什么樣的數據結構?它占用多少內存?
  • ...

這些讓人摸不著頭腦的問題,只要你耐心查找,在 stackoverflow 或者各種論壇上,一般能夠找到答案。不過,別人給出來的答案很可能是模棱兩可的,不好理解的,甚至是錯誤的。我們需要花時間甄別那些正確的、并且精準的答案,還需要花時間閱讀這些答案。有時候,即便是你得到了答案甚至記住了答案,你可能還是沒有完全理解別人給出的答案。當你需要把這樣的答案講給別人時,你會發(fā)現自己似乎無法講得清楚。

在我的職業(yè)生涯中,遇見過很多所謂的「高手」,漫長的職業(yè)生涯讓他們遇見了各種奇葩的問題,通過各種知識搜索和整理的手段,他們也記住了這些問題的答案。他們經常能拋出一些冷門的知識,知識儲備之豐富讓我嘆為觀止。但當我想深入下去時,就發(fā)現他們對事物的理解不過是一個指向別處的引用(reference),是借來(borrow)的知識,自己沒有知識的所有權(ownership),所以往往容易語焉不詳,只能給出淺層的回答。

那么,如何避免這種情況,讓自己成為知識的所有者呢?

我們要學會不依賴別人的斷言,單單通過代碼本身來探索問題的答案。作為開發(fā)者,我們最大的優(yōu)勢就是我們研究的對象,計算機和計算機軟件,就放在離我們唾手可得的地方。我們只要想辦法用代碼構造研究這個問題的實驗,就能不斷迭代夠逐漸找到答案。而且,這答案是第一手的,不是別人咀嚼后喂給你的,而是你通過實驗驗證出來的,所以它是你自己的知識,即便過了十年二十年,你依然能清晰地給出答案,或者至少給出通往這個答案的途徑。

問有意思的問題

最近在我的極客時間的專欄《陳天 · Rust 第一課》中,有個同學在看到我畫的這張圖時:

 

 

問了這樣一個問題:

虛表是每個類有一份,還是每個對象有一份,還是每個胖指針有一份?

這是一個非常棒的問題。我不知道有多少人在學習的時候會發(fā)出這樣的疑問,但我猜很少,因為至少我之前在直播講 Rust 時,在我公司內部講 Rust 時,沒有人關心過這個問題。

而 問對問題,比知道答案更重要 。一個好的問題,就已經離知識很近了。

<愛因斯坦>

如何才能問出有意思的問題?

我在學習 trait object 的時候,也問過同樣的問題,并且順著問題,找到了答案。你想想,什么樣的思考會觸發(fā)問這個問題呢?

也許來自對比學習(我自己的情況):因為 C++ 每個類有一個自己的虛表,所以不免會好奇 trait object 是不是也是類似的實現?

也許來自對內存效率的擔憂:trait object 有個指針指向虛表,那么如果在每個 trait object 生成時都生成一張?zhí)摫恚敲春芾速M內存啊。對于上面的 Write trait,還好,只有幾個方法,但對一些比較大的 trait,如 Iterator,有近七十個方法,也就是說光這些方法組成的虛表,就有五百多字節(jié)!如果每個 trait object 都自己生成這樣一張表,內存占用多可怕!所以如果不搞明白,不敢大量使用啊。

也許還有其它什么思考觸發(fā)了這個問題。

不管怎么樣,能問出好的問題,一定會現有一些先驗知識,然后通過細致的觀察,深入的思考,才會慢慢萌發(fā)問題。

從假設到通過實驗驗證假設

那么,有了好問題,我們如何解答這個問題呢?

我們可以根據自己已有的知識,思考最可能接近真相的方向,然后動手做實驗來驗證自己的假設。對于這個問題,我認為為每個 trait object 生成一張表效率太低,不太可能,所以傾向于像 C++ 那樣,每個類型都有靜態(tài)的虛表。既然我有了這樣的假設,那么怎么驗證它呢?我可以用兩個字符串分別生成 trait object,然后打印虛表的地址進行對比。如果一致,那么符合我的假設:每個類型都有靜態(tài)的虛表。

實驗一

有了這個方向,查閱資料,寫出下面的第一個實驗的代碼并非難事:

  1. use std::fmt::Debug; 
  2. use std::mem::transmute; 
  3.  
  4. fn main() { 
  5.     let s1 = String::from("hello"); 
  6.     let s2 = String::from("goodbye"); 
  7.     let w1: &dyn Debug = &s1; 
  8.     let w2: &dyn Debug = &s2; 
  9.  
  10.     // 強行把 triat object 轉換成兩個地址 (usize, usize) 
  11.     // 這是不安全的,所以是 unsafe 
  12.     let (addr1, vtable1) = unsafe { transmute::<_, (usize, usize)>(w1 as *const dyn Debug) }; 
  13.     let (addr2, vtable2) = unsafe { transmute::<_, (usize, usize)>(w2 as *const dyn Debug) }; 
  14.      
  15.     // trait object(s / Display) 的 ptr 地址和 vtable 地址 
  16.     println!("addr1: 0x{:x}, vtable1: 0x{:x}", addr1, vtable1); 
  17.     // trait object(s / Debug) 的 ptr 地址和 vtable 地址 
  18.     println!("addr2: 0x{:x}, vtable2: 0x{:x}", addr2, vtable2); 
  19.  
  20.     // String 類型擁有相同的 vtable? 
  21.     assert_eq!(vtable1, vtable2); 

如果你在 rust playground 里運行,會得到下面的結果:

addr1: 0x7ffd1c524910, vtable1: 0x556591eae4c8
addr2: 0x7ffd1c524928, vtable2: 0x556591eae4c8

從實驗一中,我們得出結論: 虛表是共享的,不是每一個 trait object 都有一張?zhí)摫?/b> 。從虛表的地址上看,它既不是堆地址,也不是棧地址。目測像是代碼段或者數據段的地址?

你看,我們通過觀測實驗結果,又有了新的發(fā)現,同時有了新的問題。

于是我們繼續(xù)迭代。

實驗二

在實驗一的基礎上,我們可以定義一個靜態(tài)變量 V,打印一下它的地址(DATA 段),以及打印一下 main() 函數的地址(TEXT 段)來比較:

  1. static V: i32 = 0
  2. println!("V: {:p}, main(): {:p}", &V, main as *const ()); 

打印結果(注意每次編譯后運行地址都會不同):

  1. addr1: 0x7fff2dd3e7f8, vtable1: 0x557a21b9e488 
  2. addr2: 0x7fff2dd3e810, vtable2: 0x557a21b9e488 
  3. V: 0x557a21b910ec, main(): 0x557a21b63e40 

Bingo!實驗二證明了我們的猜測沒錯, 虛表是編譯時就生成好,塞入二進制文件中的 。當生成 trait object 時,根據是哪個類型,再指向對應的位置。

那么,Rust 為每個類型(比如 String )編譯時只生成一個 vtable,對么?

我們目前很接近真相,但還有未解的疑問。從目前的實驗中,我們還無法得出這個結論。實驗一里,我們只用了 Debug trait,這個樣本太小,不具備普遍性。如果對同一個數據類型(比如 String)使用不同的 trait,會導致不同的結果么?我們并不知道。如果結果相同,那么我們就大概率可以確定,一個類型一張?zhí)摫?,否則,就應該是每個類型的每個 trait 實現,都有一張?zhí)摫怼?/p>

實驗三

于是在實驗三里,我們用同一個類型的兩個不同的 Trait,來生成不同的 trait object,看看其虛表是否是同一個地址:

  1. use std::fmt::{Debug, Display}; 
  2. use std::mem::transmute; 
  3.  
  4.  
  5. fn main() { 
  6.     let s1 = String::from("hello world!"); 
  7.     let s2 = String::from("goodbye world!"); 
  8.     // Display / Debug trait object for s 
  9.     let w1: &dyn Display = &s1; 
  10.     let w2: &dyn Debug = &s1; 
  11.  
  12.  
  13.     // Display / Debug trait object for s1 
  14.     let w3: &dyn Display = &s2; 
  15.     let w4: &dyn Debug = &s2; 
  16.  
  17.  
  18.     // 強行把 triat object 轉換成兩個地址 (usize, usize) 
  19.     // 這是不安全的,所以是 unsafe 
  20.     let (addr1, vtable1) = unsafe { transmute::<_, (usize, usize)>(w1 as *const dyn Display) }; 
  21.     let (addr2, vtable2) = unsafe { transmute::<_, (usize, usize)>(w2 as *const dyn Debug) }; 
  22.     let (addr3, vtable3) = unsafe { transmute::<_, (usize, usize)>(w3 as *const dyn Display) }; 
  23.     let (addr4, vtable4) = unsafe { transmute::<_, (usize, usize)>(w4 as *const dyn Debug) }; 
  24.  
  25.  
  26.     // s 和 s1 在棧上的地址,以及 main 在 TEXT 段的地址 
  27.     println!( 
  28.         "s1: {:p}, s2: {:p}, main(): {:p}"
  29.         &s1, &s2, main as *const () 
  30.     ); 
  31.     // trait object(s / Display) 的 ptr 地址和 vtable 地址 
  32.     println!("addr1: 0x{:x}, vtable1: 0x{:x}", addr1, vtable1); 
  33.     // trait object(s / Debug) 的 ptr 地址和 vtable 地址 
  34.     println!("addr2: 0x{:x}, vtable2: 0x{:x}", addr2, vtable2); 
  35.  
  36.  
  37.     // trait object(s1 / Display) 的 ptr 地址和 vtable 地址 
  38.     println!("addr3: 0x{:x}, vtable3: 0x{:x}", addr3, vtable3); 
  39.  
  40.  
  41.     // trait object(s1 / Display) 的 ptr 地址和 vtable 地址 
  42.     println!("addr4: 0x{:x}, vtable4: 0x{:x}", addr4, vtable4); 
  43.  
  44.  
  45.     // 指向同一個數據的 trait object 其 ptr 地址相同 
  46.     assert_eq!(addr1, addr2); 
  47.     assert_eq!(addr3, addr4); 
  48.  
  49.  
  50.     // 指向同一種類型的同一個 trait 的 vtable 地址相同 
  51.     // 這里都是 String + Display 
  52.     assert_eq!(vtable1, vtable3); 
  53.     // 這里都是 String + Debug 
  54.     assert_eq!(vtable2, vtable4); 

結果令人驚喜:String + Display 生成的 trait object,和 String + Debug 生成的 trait object,使用的是不同的 vtable:

  1. s1: 0x7ffc7d427a08, s2: 0x7ffc7d427a20, main(): 0x561b76ff2e90 
  2. addr1: 0x7ffc7d427a08, vtable1: 0x561b7702d3b8 
  3. addr2: 0x7ffc7d427a08, vtable2: 0x561b7702d3d8 
  4. addr3: 0x7ffc7d427a20, vtable3: 0x561b7702d3b8 
  5. addr4: 0x7ffc7d427a20, vtable4: 0x561b7702d3d8 

所以,我們可以確定, 虛表是每個 (Trait, Type) 一份,在編譯時就生成好了 。

那么,編譯器在什么時機來生成這張?zhí)摫砟兀坑欣碛赏茢?,在編譯器編譯 impl 某個 trait 的代碼時生成了虛表,比如:

  1. impl Debug for String {...} 

因為此時編譯器有生成虛表所需要的一切信息:

  • 數據如何銷毀:String 的 drop 方法的地址此時需要已經編譯得出
  • 數據的大小和對齊:此刻是 String 類型,所以大小 24 字節(jié),對齊 8 字節(jié)
  • trait 方法:在編譯 impl Debug 時就已經得到 fmt() 方法的地址

如果我是編譯器的開發(fā)者,此時不做,更待何時?所以我們可以做出這個推斷。這個推斷邏輯自洽,看上去非常合理,大概率是對的。不過要驗證起來不那么容易,除非我們繼續(xù)在 Rust 編譯器源碼中做實驗。

從實驗結果中最終得出結論

好,綜合上述三個實驗,我們的腦海中,已經可以構筑出這樣一幅圖:

 

 

此刻,我們就完美地找到了一開始的問題我們想要的答案。對于開頭的問題,我是這么回答的:

好問題。這個在講 trait 的那一課有講到。虛表在每個 impl TraitA for TypeB {} 實現時就會編譯出一份。比如 String 的 Debug 實現, String 的 Display 實現各有一份虛表,它們在編譯時就生成并放在了二進制文件中(大概是 RODATA 段中)。 所以虛表是每個 (Trait, Type) 一份。并且在編譯時就生成好了。 如果你感興趣,可以在 playground 里運行這段代碼(這是后面講 trait 時使用的代碼): https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=89311eb50772982723a39b23874b20d6 。限于篇幅,代碼就不貼了。

因為我自己通過做實驗,找到了答案,所以,我對自己的結論和推斷都很有信心。同時,因為這是我自己探索出來的知識,我并非借用別人腦海中的想法,而是對它擁有所有權,所以,我可以自如地從各個角度來構筑我的答案。

小結

在韋氏詞典中,是這么定義科學方法的:科學方法是一種有系統(tǒng)地尋求知識的程序,涉及了以下三個步驟:問題的認知與表述、實驗數據的收集、假說的構成與測試。我們在探索 Rust 的 vtable 是如何構建的過程中,使用了科學方法。它是一個不斷迭代的過程,從觀測開始,一路經歷問問題,做出假設,構建實驗來驗證假設,觀察實驗結果,提出新的問題,進一步迭代下去,直到我們形成了一個自洽的理論:

 

 

本文我們通過一個 Rust 的例子來探討這個方法。當這個方法本身跟 Rust 無關。我們在學習編程語言,使用第三方庫,構建復雜的系統(tǒng),都可以用這個方法。如果你能夠掌握和使用這個方法,那么,慢慢地你就能成為知識的所有者。

 

責任編輯:張燕妮 來源: 迷思
相關推薦

2011-06-08 00:09:30

RationalInnovate 20軟件開發(fā)

2009-08-19 09:40:57

軟件方法論

2009-06-23 09:39:22

2013-05-15 10:02:08

軟件開發(fā)開發(fā)者

2024-09-23 15:02:40

2011-09-04 15:16:45

Innovate 20Rational云計算

2009-02-13 10:00:41

面試軟件開發(fā)程序員

2019-11-22 08:00:00

軟件軟件開發(fā)

2010-11-08 09:36:23

移動軟件開發(fā)者

2012-04-27 10:36:02

OracleJava谷歌

2014-12-09 15:51:31

Android LAPI

2020-03-20 11:43:20

開發(fā)編程語言技術

2023-10-16 08:00:00

2022-09-07 11:56:53

汽車軟件

2022-05-31 17:36:25

技術面試編程

2015-05-19 09:11:32

OpenStackOpenStack貢獻

2016-12-14 14:09:56

華為軟件DevOps

2020-02-28 15:14:41

Docker大數據軟件開發(fā)

2024-03-26 10:00:00

NVIDIA開發(fā)工具Omniverse

2023-12-13 10:02:01

軟件開發(fā)框架
點贊
收藏

51CTO技術棧公眾號