github地址:https://github.com/bradyjoestar/rustnotes(歡迎star!)
pdf下載鏈接:https://github.com/bradyjoestar/rustnotes/blob/master/Rust%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.pdf
參考:
https://rustcc.gitbooks.io/rustprimer/content/ 《RustPrimer》
https://kaisery.github.io/trpl-zh-cn/ 《Rust程序設(shè)計語言-簡體中文版》
一個C語言的例子:
int* foo() {
int a; // 變量a的作用域開始
a = 100;
char *c = "xyz"; // 變量c的作用域開始
return &a;
} // 變量a和c的作用域結(jié)束
盡管可以編譯通過,但這是一段非常糟糕的代碼,變量a和c都是局部變量,函數(shù)結(jié)束后將局部變量a的地址返回,但局部變量a存在棧中,在離開作用域后,局部變量所申請的棧上內(nèi)存都會被系統(tǒng)回收,從而造成了Dangling Pointer的問題。這是一個非常典型的內(nèi)存安全問題。很多編程語言都存在類似這樣的內(nèi)存安全問題。
再來看變量c,c的值是常量字符串,存儲于常量區(qū),可能這個函數(shù)我們只調(diào)用了一次,我們可能不再想使用這個字符串,但xyz只有當(dāng)整個程序結(jié)束后系統(tǒng)才能回收這片內(nèi)存,這點讓程序員是不是也很無奈?
所以,內(nèi)存安全和內(nèi)存管理通常是程序員眼中的兩大頭疼問題。令人興奮的是,Rust卻不再讓你擔(dān)心內(nèi)存安全問題,也不用再操心內(nèi)存管理的麻煩,那Rust是如何做到這一點的?通過所有權(quán)。
3.1 所有權(quán)
3.1.1 綁定
首先必須強調(diào)下,準(zhǔn)確地說Rust中并沒有變量這一概念,而應(yīng)該稱為標(biāo)識符,目標(biāo)資源(內(nèi)存,存放value)綁定到這個標(biāo)識符。
{
let x: i32; // 標(biāo)識符x, 沒有綁定任何資源
let y: i32 = 100; // 標(biāo)識符y,綁定資源100
}
Rust并不會像其他語言一樣可以為變量默認(rèn)初始化值,Rust明確規(guī)定變量的初始值必須由程序員自己決定。
上述代碼中,let關(guān)鍵字并不只是聲明變量的意思,它還有一層特殊且重要的概念-綁定。通俗的講,let關(guān)鍵字可以把一個標(biāo)識符和一段內(nèi)存區(qū)域做“綁定”,綁定后,這段內(nèi)存就被這個標(biāo)識符所擁有,這個標(biāo)識符也成為這段內(nèi)存的唯一所有者。
3.1.2 作用域
rust有著和其它語言類型的定義作用域的規(guī)則。
像C語言一樣,在局部變量離開作用域后,變量隨即會被銷毀;但不同是,Rust會連同變量綁定的內(nèi)存,不管是否為常量字符串,連同所有者變量一起被銷毀釋放。
3.1.3 移動語義
在Rust中,和“綁定”概念相輔相成的另一個機制就是“轉(zhuǎn)移move所有權(quán)”,意思是,可以把資源的所有權(quán)(ownership)從一個綁定轉(zhuǎn)移(move)成另一個綁定,這個操作同樣通過let關(guān)鍵字完成,和綁定不同的是,=兩邊的左值和右值均為兩個標(biāo)識符:
語法:
let 標(biāo)識符A = 標(biāo)識符B; // 把“B”綁定資源的所有權(quán)轉(zhuǎn)移給“A”
move前后的內(nèi)存示意如下:
Before move:
a <=> 內(nèi)存(地址:A,內(nèi)容:"xyz")
After move:
a
b <=> 內(nèi)存(地址:A,內(nèi)容:"xyz")
move后,如果變量A和變量B離開作用域,所對應(yīng)的內(nèi)存會不會造成“Double Free”的問題?答案是否定的,Rust規(guī)定,只有資源的所有者銷毀后才釋放內(nèi)存,而無論這個資源是否被多次move,同一時刻只有一個owner,所以該資源的內(nèi)存也只會被free一次。
3.1.4 Copy特性
舉例如下:
let a: i32 = 100;
let b = a;
println!("{}", a);
編譯確實可以通過,輸出為100。這是為什么呢,是不是跟move小節(jié)里的結(jié)論相悖了? 其實不然,這其實是根據(jù)變量類型是否實現(xiàn)Copy特性決定的。對于實現(xiàn)Copy特性的變量,在move時會拷貝資源到新內(nèi)存區(qū)域,并把新內(nèi)存區(qū)域的資源binding為b。特性有點類似于interface,但是在rust中必須顯式實現(xiàn)。
Before move:
a <=> 內(nèi)存(地址:A,內(nèi)容:100)
After move:
a <=> 內(nèi)存(地址:A,內(nèi)容:100)
b <=> 內(nèi)存(地址:B,內(nèi)容:100)
在Rust中,基本數(shù)據(jù)類型(Primitive Types)均實現(xiàn)了Copy特性,包括i8, i16, i32, i64, usize, u8, u16, u32, u64, f32, f64, (), bool, char等等。
3.1.5 淺拷貝與深拷貝
對于基本數(shù)據(jù)類型來說,“深拷貝”和“淺拷貝“產(chǎn)生的效果相同。對于引用對象類型來說,”淺拷貝“更像僅僅拷貝了對象的內(nèi)存地址。 如果我們想實現(xiàn)對String的”深拷貝“怎么辦? 可以直接調(diào)用String的Clone特性實現(xiàn)對內(nèi)存的值拷貝而不是簡單的地址拷貝。
3.1.6 高級copy
一旦一種類型實現(xiàn)了Copy特性,這就意味著這種類型可以通過的簡單的位(bits)拷貝實現(xiàn)拷貝。從前面知識我們知道“綁定”存在move語義(所有權(quán)轉(zhuǎn)移),但是,一旦這種類型實現(xiàn)了Copy特性,會先拷貝內(nèi)容到新內(nèi)存區(qū)域,然后把新內(nèi)存區(qū)域和這個標(biāo)識符做綁定。
哪些情況下我們自定義的類型(如某個Struct等)可以實現(xiàn)Copy特性? 只要這種類型的屬性類型都實現(xiàn)了Copy特性,那么這個類型就可以實現(xiàn)Copy特性。 例如:
struct Foo { //可實現(xiàn)Copy特性
a: i32,
b: bool,
}
struct Bar { //不可實現(xiàn)Copy特性
l: Vec<i32>,
}
因為Foo的屬性a和b的類型i32和bool均實現(xiàn)了Copy特性,所以Foo也是可以實現(xiàn)Copy特性的。但對于Bar來說,它的屬性l是Vec<T>類型,這種類型并沒有實現(xiàn)Copy特性,所以Bar也是無法實現(xiàn)Copy特性的。
那么我們?nèi)绾蝸韺崿F(xiàn)Copy特性呢?有兩種方式可以實現(xiàn)。
3.1.6.1.通過derive讓Rust編譯器自動實現(xiàn)
#[derive(Copy, Clone)]
struct Foo {
a: i32,
b: bool,
}
3.1.6.2 手動實現(xiàn) 不推薦 會進入unsafe rust
3.1.7 Copy trait與Clone trait
Copy內(nèi)部沒有方法,Clone內(nèi)部有兩個方法。
1.Copy trait 是給編譯器用的,告訴編譯器這個類型默認(rèn)采用 copy 語義,而不是 move 語義。Clone trait 是給程序員用的,我們必須手動調(diào)用clone方法,它才能發(fā)揮作用。
2.Copy trait不是你想實現(xiàn)就實現(xiàn),它對類型是有要求的,有些類型就不可能 impl Copy。Clone trait 沒有什么前提條件,任何類型都可以實現(xiàn)(unsized 類型除外)。
3.Copy trait規(guī)定了這個類型在執(zhí)行變量綁定、函數(shù)參數(shù)傳遞、函數(shù)返回等場景下的操作方式。即這個類型在這種場景下,必然執(zhí)行的是“簡單內(nèi)存拷貝”操作,這是由編譯器保證的,程序員無法控制。Clone trait 里面的 clone 方法究竟會執(zhí)行什么操作,則是取決于程序員自己寫的邏輯。一般情況下,clone 方法應(yīng)該執(zhí)行一個“深拷貝”操作,但這不是強制的,如果你愿意,也可以在里面啟動一個人工智能程序,都是有可能的。
5.如果你確實需要Clone trait執(zhí)行“深拷貝”操作,編譯器幫我們提供了一個工具,我們可以在一個類型上添加#[derive(Clone)],來讓編譯器幫我們自動生成那些重復(fù)的代碼。
正因為如此,在希望讓一個類型具有 Copy 性質(zhì)的時候,一般使用 #[derive(Copy, Clone)] 這種方式,這種情況下它們倆最好一起出現(xiàn),避免手工實現(xiàn) Clone 導(dǎo)致錯誤。
3.1.8 高級move
move關(guān)鍵字常用在閉包中,強制閉包獲取所有權(quán)。
fn main() {
let mut x: String = String::from("abc");
let mut some_closure = move |c: char| x.push(c);
let y = some_closure('d');
println!("x={:?}", x);
}
上述代碼會報錯。
這是因為move關(guān)鍵字,會把閉包中的外部變量的所有權(quán)move到包體內(nèi),發(fā)生了所有權(quán)轉(zhuǎn)移的問題,所以println訪問x會如上錯誤。如果我們?nèi)サ魀rintln就可以編譯通過。
那么,如果我們想在包體外依然訪問x,即x不失去所有權(quán),怎么辦?
fn main() {
let mut x: String = String::from("abc");
{
let mut some_closure = |c: char| x.push(c);
some_closure('d');
}
println!("x={:?}", x); //成功打?。簒="abcd"
}
我們只是去掉了move,去掉move后,包體內(nèi)就會對x進行了可變借用,而不是“剝奪”x的所有權(quán),細(xì)心的同學(xué)還注意到我們在前后還加了{(lán)}大括號作用域,是為了作用域結(jié)束后讓可變借用失效,這樣println才可以成功訪問并打印我們期待的內(nèi)容。
最新的版本不加大括號也可以的?但是為了可讀加上大括號比較好。盡可能滿足作用域內(nèi)部的規(guī)則。
具體內(nèi)容需要查看閉包的可變借用。
3.2 引用和借用
所有權(quán)系統(tǒng)允許我們通過“Borrowing”的方式達(dá)到這個目的。這個機制非常像其他編程語言中的“讀寫鎖”,即同一時刻,只能擁有一個“寫鎖”,或只能擁有多個“讀鎖”,不允許“寫鎖”和“讀鎖”在同一時刻同時出現(xiàn)。當(dāng)然這也是數(shù)據(jù)讀寫過程中保障一致性的典型做法。只不過Rust是在編譯中完成這個(Borrowing)檢查的,而不是在運行時,這也就是為什么其他語言程序在運行過程中,容易出現(xiàn)死鎖或者野指針的問題。
通過&符號完成Borrowing:
fn main() {
let x: Vec<i32> = vec!(1i32, 2, 3);
let y = &x;
println!("x={:?}, y={:?}", x, y);
}
Borrowing(&x)并不會發(fā)生所有權(quán)moved,所以println可以同時訪問x和y。 通過引用,就可以對普通類型完成修改。
fn main() {
let mut x: i32 = 100;
{
let y: &mut i32 = &mut x;
*y += 2;
}
println!("{}", x);
}
y在大括號結(jié)束后會釋放掉。
& 符號就是 引用,它們允許你使用值但不獲取其所有權(quán)。將獲取引用作為函數(shù)參數(shù)稱為 借用(borrowing)。
3.2.1 借用和引用的規(guī)則
1.同一作用域,特定數(shù)據(jù)最多只有一個可變借用(&mut T),或者2。
2.同一作用域,特定數(shù)據(jù)可有0個或多個不可變借用(&T),但不能有任何可變借用。
3.借用在離開作用域后釋放。
4.在可變借用釋放前不可訪問源變量。
3.2.2 引用的可變性
Borrowing也分“不可變借用”(默認(rèn),&T)和“可變借用”(&mut T)。
顧名思義,“不可變借用”是只讀的,不可更新被引用的內(nèi)容。
fn main() {
//源變量x可變性
let mut x: Vec<i32> = vec!(1i32, 2, 3);
//只能有一個可變借用
let y = &mut x;
// let z = &mut x; //錯誤
y.push(100);
//ok
println!("{:?}", y);
//錯誤,可變借用未釋放,源變量不可訪問
// println!("{:?}", x);
} //y在此處銷毀
另外一個例子:
fn main() {
let mut x: Vec<i32> = vec!(1i32, 2, 3);
//更新數(shù)組
//push中對數(shù)組進行了可變借用,并在push函數(shù)退出時銷毀這個借用
x.push(10);
{
//可變借用1
let mut y = &mut x;
y.push(100);
//可變借用2,注意:此處是對y的借用,不可再對x進行借用,
//因為y在此時依然存活。
let z = &mut y;
z.push(1000);
println!("{:?}", z); //打印: [1, 2, 3, 10, 100, 1000]
} //y和z在此處被銷毀,并釋放借用。
//訪問x正常
println!("{:?}", x); //打印: [1, 2, 3, 10, 100, 1000]
}
3.2.3 總結(jié)
1.借用不改變內(nèi)存的所有者(Owner),借用只是對源內(nèi)存的臨時引用。
2.在借用周期內(nèi),借用方可以讀寫這塊內(nèi)存,所有者被禁止讀寫內(nèi)存;且所有者保證在有“借用”存在的情況下,不會釋放或轉(zhuǎn)移內(nèi)存。
3.失去所有權(quán)的變量不可以被借用(訪問)。
4.在租借期內(nèi),內(nèi)存所有者保證不會釋放/轉(zhuǎn)移/可變租借這塊內(nèi)存,但如果是在非可變租借的情況下,所有者是允許繼續(xù)非可變租借出去的。
5.借用周期滿后,所有者收回讀寫權(quán)限。
6.借用周期小于被借用者(所有者)的生命周期。
3.3 生命周期
幾個概念:
1.Owner: 資源的所有者 a
2.Borrower: 資源的借用者 x
3.Scope: 作用域,資源被借用/引用的有效期
無論是資源的所有者還是資源的借用/引用,都存在在一個有效的存活時間或區(qū)間,這個時間區(qū)間稱為生命周期, 也可以直接以Scope作用域去理解。
例子:
fn main() {
let a = 100_i32;
{
let x = &a;
} // x 作用域結(jié)束
println!("{}", x);
}
分析:借用者 x 的生命周期是資源所有者 a 的生命周期的子集。但是 x 的生命周期在第一個 }時結(jié)束并銷毀,在接下來的 println! 中再次訪問便會發(fā)生嚴(yán)重的錯誤。
3.3.1 隱式lifetime
我們經(jīng)常會遇到參數(shù)或者返回值為引用類型的函數(shù):
fn foo(x: &str) -> &str {
x
}
foo 函數(shù)僅僅接受一個 &str 類型的參數(shù)(x為對某個string類型資源Something的借用),并返回對資源Something的一個新的借用。
實際上,上面函數(shù)包含隱性的生命周期命名,這是由編譯器自動推導(dǎo)的,相當(dāng)于:
fn foo<'a>(x: &'a str) -> &'a str {
x
}
3.4 高級所有權(quán)
前面三小節(jié)未大量涉及到關(guān)于所有權(quán)中的比較高級用法。
3.4.1.函數(shù)傳遞參數(shù)和返回參數(shù)類似于let語句
在rust中,函數(shù)是存放在函數(shù)棧,為了更為快速的運行。
函數(shù)輸入?yún)?shù)的傳遞和返回參數(shù)的賦值類似于let語句,都看傳遞的參數(shù)和返回的參數(shù)是否實現(xiàn)了copy trait。
如果實現(xiàn)了copy trait,那么就不會奪走它的所有權(quán),標(biāo)識符在函數(shù)外部還可以繼續(xù)訪問。
如果沒有實現(xiàn)copy trait,那么它的所有權(quán)都會被奪走。
需要說明的是,從結(jié)果上來看,引用是值傳遞,和實現(xiàn)了copy trait的表征相同,同樣可以外部繼續(xù)使用。
舉例而言:
pub fn test(){
let a = vec![1,2,3,4,5];
print_vec(a);
//error[E0382]: borrow of moved value: `a`
// --> src/fn_params.rs:6:21
// |
//2 | let a = vec![1,2,3,4,5];
// | - move occurs because `a` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
//3 |
//4 | print_vec(a);
// | - value moved here
//5 |
//6 | println!("{:?}",a);
// | ^ value borrowed here after move
// println!("{:?}",a);
{
let a = vec![1,2,3,4,5];
let b = &a;
print_vecs(b);
//no error
println!("{:?}",b);
}
}
fn print_vec(a: Vec<i32>){
println!("{:?}",a);
}
fn print_vecs(a:&Vec<i32>){
println!("{:?}",a)
}
從上面可以看出,以vec!為函數(shù)參數(shù)則發(fā)生了move,在后面無法使用。
以&vec!為函數(shù)參數(shù),后續(xù)仍然可以使用&vec!。
錯誤提示中也是說明根據(jù)是否實現(xiàn)copy trait來決定是否進行所有權(quán)的轉(zhuǎn)移。
3.4.2 涉及到函數(shù)和結(jié)構(gòu)體的借用檢查器
在rust中引入引用后,我們需要使用引入借用檢查器來保證引用的生命周期不會超過所有權(quán)的生命周期。
Rust 編譯器有一個 借用檢查器(borrow checker),它比較作用域來確保所有的借用都是有效的。避免出現(xiàn)類似C的懸掛指針等問題。借用檢查器是在編譯階段進行工作的,將所有的無效借用識別出來。
大部分情況下,借用檢查器可以正常工作,對于某些特殊情況,借用檢查器無法識別,需要由開發(fā)人員顯示標(biāo)注生命周期。
主要分為以下三種情況:
1.函數(shù)定義中的生命周期注解
2.結(jié)構(gòu)體定義中的生命周期注解
3.方法定義中的生命周期注解
上述三種類型的生命周期注解主要用來為借用檢查器提供幫助,查看使用到它們的地方是否滿足借用的生命周期規(guī)則,有沒有懸掛指針的問題。
3.4.2.1 函數(shù)定義中的生命周期注解
舉例而言:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
rust程序設(shè)計語言 簡體中文版已經(jīng)描述的非常到位了。
當(dāng)在函數(shù)中使用生命周期注解時,這些注解出現(xiàn)在函數(shù)簽名中,而不存在于函數(shù)體中的任何代碼中。這是因為 Rust 能夠分析函數(shù)中代碼而不需要任何協(xié)助,不過當(dāng)函數(shù)引用或被函數(shù)之外的代碼引用時,讓 Rust 自身分析出參數(shù)或返回值的生命周期幾乎是不可能的。這些生命周期在每次函數(shù)被調(diào)用時都可能不同。這也就是為什么我們需要手動標(biāo)記生命周期。
當(dāng)具體的引用被傳遞給 longest 時,被 'a 所替代的具體生命周期是 x 的作用域與 y 的作用域相重疊的那一部分。換一種說法就是泛型生命周期 'a 的具體生命周期等同于 x 和 y 的生命周期中較小的那一個。因為我們用相同的生命周期參數(shù) 'a 標(biāo)注了返回的引用值,所以返回的引用值就能保證在x和y中較短的那個生命周期結(jié)束之前保持有效。
要推導(dǎo)Lifetime是否合法,先明確兩點:
1.輸出值(也稱為返回值)依賴哪些輸入值
2.輸入值的Lifetime大于或等于(可能依賴的輸出值)的Lifetime (準(zhǔn)確來說:子集,而不是大于或等于)
Lifetime推導(dǎo)公式: 當(dāng)輸出值R依賴輸入值X Y Z ...,當(dāng)且僅當(dāng)輸出值的Lifetime為所有輸入值的Lifetime交集的子集時,生命周期合法。
例子1:
fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
if true {
x
} else {
y
}
}
因為返回值同時依賴輸入?yún)?shù)x和y,所以
Lifetime(返回值) ? ( Lifetime(x) ∩ Lifetime(y) )
即:
'a ? ('a ∩ 'a) // 成立
例子2:
fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
if true {
x
} else {
y
}
}
因為返回值同時依賴x和y,所以
Lifetime(返回值) ? ( Lifetime(x) ∩ Lifetime(y) )
即:
'a ? ('a ∩ 'b) //不成立
很顯然,上面我們根本沒法保證成立。
所以,這種情況下,我們可以顯式地告訴編譯器'b比'a長('a是'b的子集),只需要在定義Lifetime的時候, 在'b的后面加上: 'a, 意思是'b比'a長,'a是'b的子集:
fn foo<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
if true {
x
} else {
y
}
}
這里我們根據(jù)公式繼續(xù)推導(dǎo):
條件:Lifetime(x) ? Lifetime(y)
推導(dǎo):Lifetime(返回值) ? ( Lifetime(x) ∩ Lifetime(y) )
即:
條件: 'a ? 'b
推導(dǎo):'a ? ('a ∩ 'b) // 成立
上面是成立的,所以可以編譯通過。
3.4.2.2 結(jié)構(gòu)體中的生命周期
在struct中Lifetime同樣重要。關(guān)于rust中struct使用請查看第四章。
我們來定義一個Person結(jié)構(gòu)體。
struct Person {
age: &u8,
}
編譯時我們會得到一個error:
<anon>:2:8: 2:12 error: missing lifetime specifier [E0106]
<anon>:2 age: &str,
之所以會報錯,這是因為Rust要確保Person的Lifetime不會比它的age借用長,不然會出現(xiàn)Dangling Pointer的嚴(yán)重內(nèi)存問題。而在結(jié)構(gòu)體中我們又沒有進行指定。所以我們需要為age借用聲明Lifetime:
struct Person<'a> {
age: &'a u8,
}
不需要對Person后面的<'a>感到疑惑,這里的'a并不是指Person這個struct的Lifetime,僅僅是一個泛型參數(shù)而已,struct可以有多個Lifetime參數(shù)用來約束不同的field,實際的Lifetime應(yīng)該是所有fieldLifetime交集的子集。
fn main() {
let x = 20_u8;
let stormgbs = Person {
age: &x,
};
}
這里,生命周期/Scope的示意圖如下:
{ x stormgbs * }
所有者 x: |________________________|
所有者 stormgbs: |_______________| 'a
借用者 stormgbs.age: |_______________| stormgbs.age = &x
3.4.2.3 結(jié)構(gòu)體方法定義的生命周期
既然<'a>作為Person的泛型參數(shù),所以在為Person實現(xiàn)方法時也需要加上<'a>,不然:
impl Person {
fn print_age(&self) {
println!("Person.age = {}", self.age);
}
}
報錯:
<anon>:5:6: 5:12 error: wrong number of lifetime parameters: expected 1, found 0 [E0107]
<anon>:5 impl Person {
^~~~~~
正確的做法是:(age是有生命周期的)
impl<'a> Person<'a> {
fn print_age(&self) {
println!("Person.age = {}", self.age);
}
}
這樣加上<'a>后就可以了。讀者可能會疑問,為什么print_age中不需要加上'a?這是個好問題。因為print_age的輸出參數(shù)為(),也就是可以不依賴任何輸入?yún)?shù), 所以編譯器此時可以不必關(guān)心和推導(dǎo)Lifetime。即使是fn print_age(&self, other_age: &i32) {...}也可以編譯通過。
如果Person的方法存在輸出值(借用)呢?
impl<'a> Person<'a> {
fn get_age(&self) -> &u8 {
self.age
}
}
get_age方法的輸出值依賴一個輸入值&self,這種情況下,Rust編譯器可以自動推導(dǎo)為:
impl<'a> Person<'a> {
fn get_age(&'a self) -> &'a u8 {
self.age
}
}
如果輸出值(借用)依賴了多個輸入值呢?
impl<'a, 'b> Person<'a> {
fn get_max_age(&'a self, p: &'a Person) -> &'a u8 {
if self.age > p.age {
self.age
} else {
p.age
}
}
}
類似之前的Lifetime推導(dǎo)章節(jié),當(dāng)返回值(借用)依賴多個輸入值時,需顯示聲明Lifetime。和函數(shù)Lifetime同理。
其他無論在函數(shù)還是在struct中,甚至在enum中,Lifetime理論知識都是一樣的。希望大家可以慢慢體會和吸收,做到舉一反三。