沮丧吗?不是你的问题,是铁锈的问题

2020-08-15 05:17:45

学习生锈是..。一种体验。一次情感之旅。在我努力学习“锈”的最初几个月里,我几乎没有比这更沮丧的了。

更糟糕的是,不管你以前有多少Java、C#、C++或其他方面的经验,它仍然会让你感到不安。

事实上,更多的经验可能会让事情变得更糟!习惯已经变得更深了,而且有一种特定的期望,那就是,到现在,你应该能够在更短的时间内完成这件事。

也许,在成功发布代码多年之后,您不再像刚开始时那样有同样的好奇心、同样的坦率和意愿去感觉丢失了。

学习“铁锈”会让你觉得自己又像个初学者了--为什么这么难呢?这并不觉得应该有那么难。我以前也做过类似的事情。我知道我想要什么。现在我只需要..。让它发生吧。

我会继续在我所有的初学者级别的文章中包括这样的介绍,因为它们非常重要:如果你染上了锈,就会受到阻碍。告诉你,你很快就会跟上速度的,那就是大错特错,而我也不是大话王。

然而,有一个非常好的原因是学习如此之难。当你从另一种语言切换到Rust时,你并不是从法语切换到西班牙语-你不仅仅是在学习新的词汇,这样你就可以说出相同的东西,只是它们的拼写和发音不同。

你正在学习新的词汇,并学会谈论你以前从未讨论过的话题。你正在学习一种全新的沟通方式。演讲(口语或书面语)对我们很多人来说都是非常重要的,从头开始是非常令人不安的。

你会遇到一些你不能用你的任何先验知识来框定的问题。写铁锈需要遵守一套规则,而这些规则你是无法用其他语言的类比来描述的。这又增加了另一个级别的难度:通常情况下,你甚至不能描述出哪里出了问题,无法得到一些帮助。

通用搜索引擎在解决锈蚀问题方面相当没用。您最好的选择几乎就是Rust编译器本身,以及它的诊断功能。或者,咬紧牙关,接受这样一个事实:在你回到你想要做的事情之前,你将不得不回去阅读更多的初学者水平的材料,然后才能有一个瞬间。

然而,编译器只能做到这一点--因为它不仅面临着解释在其他语言中没有等价物的概念的困难,而且:它是从你的代码而不是你的大脑开始工作的。

当你把你脑海中的东西放入代码中时,细节就会丢失--虽然这些在其他语言中可能无关紧要,但在Rust中,它们非常重要。

如果您做了大量的动态类型/弱类型工作,这一点尤其正确。

您来自Python、Ruby或JavaScript等语言,因此习惯于编写如下所示的函数:

当你有一个这样的函数时,你知道只在可以加在一起的东西上调用它。例如,数字。你知道不应该用这样的话来称呼它。对象或字典,因为那样的结果可能是无意义的。

然而,铁锈并不是那么聪明。首先,它真的希望所有的东西都有一种类型:

但是,如果我们想要使我们的add函数对任何两个可以相加的东西起作用,我们必须使我们的函数成为泛型函数-这是它自己的兔子洞。

错误[E0369]:无法将`T`添加到`T`-->;src/main.rs:6:7|6|a+b|-^-T|T|Help:考虑限制类型参数`T`|5|fn add<;T:std::ops::add<;output=T>;>;(a:t,b:t)->;T{|^。

那里的帮助部分就是关于钱的--问题的核心是铁锈赢了--除非它确实知道可以加两样东西,否则我们就不加这两样东西了。

使用std::ops::add;fn main(){println!(";ten={}";,add(4,6));}fn add<;T>;(a:t,b:t)->;T其中T:add<;output=T>;,{a+b}。

所以-你比拉斯特聪明。铁锈只知道你说了什么。你最好也清楚你的意思!

但这也有一个好处:花时间仔细地向拉斯特描述你的意思,可以避免很多错误。它可以防止整个班级的错误。

在这个简单示例中,很明显:由于Rust在默认情况下没有对字符串进行所有类型的隐式强制,因此您永远不会得到意外的[Object Object][Object Object]。

而且,最重要的是,如果要将添加函数作为机箱的一部分发布(NPM包、gem、.。蛋?。Y';所有人,Python还好吗?),其他人也不可能以意外的[对象对象][对象对象]而告终。

因为这些类型不仅仅是建议性的--它们还是您的库接口的一部分,即使它被用作另一个项目的一部分。

如果您已经习惯了更动态/弱类型的语言,那么很长一段时间以来,您一直在维护不变量--可能从未使用过不变量这个词。

您也可以将其称为假设&在调用add的整个持续时间内,我们假设a和b可以相加。从它永远不会改变的意义上说,它是一个不变的东西。如果在某一时刻,a或b变成不能相加的值,那么我们的代码将是错误的。

对于错误也有一个更专业的术语-维护不变量就是维护健全性。破坏不变量的代码称为不健全代码。

在Rust中,我们没有牢记不变量,而是直接将它们保存在代码中。这允许编译器在编译时强制执行它们。

您还可以将不变量视为永久断言。例如,C代码往往包含很多运行时断言-如果我们到达代码的这一部分,那么";ptr";不能为空。

不过,奖品是试图从一开始就阻止无效程序编译-尽早发现问题。对于难以描述的问题或涉及不受控制的用户输入的情况,仅求助于运行时错误。

您可能会对前面的例子提出异议。你可能会发现你自己想要争辩这样的函数:

//(再次未编译)fn add<;T>;(a:t,b:t)->;T{a+b}。

...拥有Rust约束类型T本身所需的所有信息,因此只能使用可以一起相加的值来调用Add。

Fn Get_Some_Numbers()->;vec<;usize>;{vec![1,2,3]}fn main(){//此`let`绑定现在具有显式类型:let v:vec<;usize>;=Get_Some_Numbers();}。

#include<;stdint.h>;#include<;stdio.h>;char*Humanize_number(Size_T N){开关(N){案例0:return";零";;案例1:return";one";;案例2:return";Two";;}}int main(){printf(";0=%s\n";,Humanize_number(0));printf(";1=%s\n";,Humanize_number(1));printf(";2=%s\n";,Humanize_number(2));printf(";3=%s\n";,Humanize_number(3));返回0;}。

$GCC Main.c-o Main&;./Main0=零1=One2=两个[1]148103分段故障(核心转储)。/Main。

C编译器知道这段代码有问题。如果我们向-WALL询问它的意见,它会告诉我们:

$GCC-Wall main.c-o main.c:in函数‘Humanize_number’:main.c:13:1:警告:控件到达非空函数[-Wreturn-type]13|}|^。

Fn main(){println!(";0={}";,Humanize_number(0));println!(";1={}";,Humanize_number(1));println!(";2={}";,Humanize_number(2));println!(";3={}";,Humanize_number(3));}fn Humanize_number(n:usize)->;&;';静态字符串{Match n{0=>;";,0";,1=>;";,2=>;";,}}

错误[E0004]:非穷举模式:`_`未覆盖-->;src/main.rs:9:11|9|匹配n{|^模式`_`未覆盖|=help:确保处理所有可能的情况,可能是通过添加通配符或更多匹配臂=注意:匹配值的类型为`usize`。

即使没有人使用0、1或2以外的值调用Humanize_number,这对Rust来说也无关紧要。它不会让您按原样编译代码。

由于usize类型的值范围从0到40亿(在32位上),或0到18百万分之一(即180亿),它希望您确保每个案例都得到处理。

Fn Humanize_Number(n:usize)->;&;';静态字符串{Match n{0=>;";,0";,1=>;";,2=>;";,_=>;死机!(";n太大";),}}。

Fn Humanize_Number(n:usize)->;&;';静态字符串{Match n{0=>;";,1=>;";,1=>;";,2=>;";,_=>;";大数字&34;,}}。

Struct NumberTooBig;FN Humanize_Number(n:usize)->;result<;&;&39;static str,NumberTooBig>;{Match n{0=>;OK(";零";),1=>;OK(";one";),2=>;OK(";Two";),_=>;Err(。

Fn main(){println!(";0={}";,Humanize_number(0).unwork_or(";a Big number";));println!(";1={}";,Humanize_number(1).unwork_or(";a Big number";));println!(";2={}";)。,Humanize_number(2).unwork_or(";a Big number";));println!(";3={}";,Humanize_number(3).unwork_or(";a Big number";));}。

为什么Rust不想让我们编写在某些情况下可以工作的代码,而不是其他情况下可以工作的代码?因为在这种情况下,立即出现分段故障是我们所能期待的最好结果。

如果我们实际将Humanize_number的结果存储在某个地方,并在以后使用它,那么问题会变得更加严重。或者,如果我们最终将其传递给需要有效字符串的函数。所有的不变量都会被打破,谁知道会发生什么呢?

嗯。或者泄露客户的私人数据。或者让手术机器人大发雷霆。可能会发生很多不好的事情。

但这并没有回答我们最初的问题:为什么拉斯特可以在这里推断出v的类型:

Fn main(){//推导为`vec<;u8>;`let v=vec![0u8,3u8,5u8];}。

对于初学者来说,指定类型和这些类型的界限不仅仅对函数的调用者有用。

使用std::ops::add;fn main(){let a=vec![0,1];let b=vec![2,3];//!对不能相加的值调用`add`,让c=add(a,b);}fn add<;T>;(a:t,b:t)->;T其中T:add<;output=T>;,{a+b}。

错误[E0277]:无法将`std::VEC::VEC<;{INTEGER}>;`添加到`STD::VEC::VEC<;{INTEGER}>;`-->;src/main.rs:6:13|6|let c=add(a,b);|^^`std::vec::vec<;{INTEGER}>;+std:(a:T,b:t)->;T|-此10|where 11|T:add<;output=T>;,|-`add`|=help:`std::ops::Add`未为`std::VEC::VEC<;{INTEGER}&gT;`实现特性`std::ops::Add`。

使用std::ops::add;fn main(){让14th=add(7,7);dbg!(十四);}fn add<;T>;(a:t,b:t)->;T其中T:add<;output=T>;,{//从a中减去b,但我们只要求可以添加的类型//!A-b}

货物检查--安静[E0369]:无法从`T`减去`T`-->;src/main.rs:12:7|12|a-b|-^-T|T|Help:考虑进一步限制此界限|10|T:add<;output=T&>;+std::ops::sub<;output=T&>;,|^。

在这一点上,我们正危险地接近于与学术论文调情,所以让我们立即举个例子。

锈病有一个Into特征,它描述了一种类型转化为另一种类型的能力。它与强制转换(as运算符)不同;您实际上必须调用into()方法:

Fn main(){设a:u8=255;设b:u16=a。Into();设c:u32=a。变成();设d:u64=a。INTO();DBG!(a、b、c、d);}。

$Cargo运行--安静[src/main.rs:8]a=255[src/main.rs:8]b=255[src/main.rs:8]c=255[src/main.rs:8]d=255。

在此代码示例中,a是一个无符号8位整数,我们使用相同的方法将其转换为无符号16位整数、无符号32位整数和无符号64位整数,所有这些都使用相同的方法:Into::Into。

这意味着into::into可以返回不同的类型,不仅取决于接收方是什么类型(在所有三个调用中都是U8),而且还取决于预期的类型。

Fn main(){设a:u8=255;设b=a。Into();println!(";b={}";,b);}

$Cargo Run--quieteric[E0282]:需要类型批注-->;src/main.rs:4:9|4|let b=a.into();|^考虑给`b`一个类型。

显然,我们需要一个类型,让我们称其为B,对于该类型,A存在Iml Into B&>,B也存在Imp Display,因为我们在println!中使用它。打电话。

但是有很多这样的类型-u16、u32、u64、u128、i16、i32、i64和i128都可以很好地工作。

请注意,i8不起作用,因为它不能表示所有可能的u8值。在这种情况下,我们必须使用TryIntotrait,它代表尝试转换容易出错的操作的能力。

由于我们讨论的主题是整数类型,因此该规则有一个值得注意的例外。在此代码中:

我们得到了一辆Vec<;I32>;。整数文字不是特定类型,它们是{整数}。如果需要特定类型,则它们可以变为U64、I8或其他任何类型-但如果不是,则默认为I32。这是我能想到的唯一例外。

Rust编译器有很多关于类型、它们可能的值以及它们能够做的事情的知识(在很大程度上是:它们实现的特性)。

它一直使用这些知识来推断变量绑定、文字和类型参数的类型(fn add<;T>;中的T)。

但是,Rust编译器所能做的演绎是有限制的。当它开始看起来太像猜测时,它会要求更明确的指令型注释。

//(不编译)struct Wolf{}Impl Wolf{FN greet(&;self){println!(";awoooo&34;);}}struct蜥蜴{}Impll Lizard{FN greet(&;self){println!(";*chirp chirp*&34;);}}FN Acquisition_Pet<;T>;(comfy:bofy。T{if comfy{wolf{}}Else{蜥蜴{}fn main(){让PET=Acquisition_Pet(True);Pet。Greet();}。

错误[E0282]:需要类型批注-->;src/main.rs:27:5|26|let PET=Acquisition_Pet(True);|-考虑给`pet`一个类型27|pet.greet();|^无法推断类型|=备注:此时必须知道类型。

错误[E0308]:类型不匹配-->;src/main.rs:19:9|17|fn Acquisition_Pet<;T>;(comfy:bool)->;T{|--由于返回类型而应为`T`|此类型参数18|if comfy{19|wolf{}|^预期类型参数`T`,找到结构`Wolf`|=Note:应为类型参数`T。(comfy:bool)->;T{|--返回类型需要`T`|此类型参数...21|蜥蜴{}|^需要类型参数`T`,找到结构`Lizard`|=备注:需要的类型参数`T`找到结构`Lizard`。

现在的问题是什么?Acquisition_pet是泛型的-显然,它可以返回不同的类型。我们用true来调用它,所以很明显,它应该返回一个Wolf,而且我们也希望返回一个Wolf(这是我们在main函数中给我们的petbinding指定的类型)。

Fn Ask_Comfy_Preference()->;bool{println!(";你喜欢舒适的宠物吗?(是或否)";);让mut Answer=string::new();std::IO::stdin()。读取行(&A;MUT应答)。展开();匹配答案。Trim(){";yes";=>;true,";no";=>;false,_=>;{Panic!(";抱歉,我听不懂您的回答:{:?}";,Answer);}fn main(){let comfy=Ask_comfy_Preference();let PET=Acquisition_PET(Comfy);PET。Greet();}。

现在宠物的类型取决于用户输入。在具有动态类型的语言中,这根本不是问题。但在这里,没有鸭子可以嘎嘎叫,也不能像鸭子一样走路。

狼和蜥蜴都有打招呼的方法,这并不重要,它们在结构上的相似性一点也不相关。

唯一重要的是代码的各个部分都同意维护的合同。

在Rust标准库中有一种类型,可以让我们返回任何东西。嗯,这是一个特性:任何。

//错误代码ahoy fn Acquisition_PET(comfy:bool)->;dyn std::any::any{if comfy{wolf{}}Else{蜥蜴{}。

这不起作用-我们不能只使用特征作为返回类型,就像这样:

尝试编译该代码会给您很多建议。编译器想要同时给你上一堂很多东西的速成课程--所以为了不再吓跑你,我就不给你看了。

Fn Acquisition_PET(comfy:bool)->;Iml std::any::any{if comfy{wolf{}}Else{蜥蜴{}。

这一次,我们承诺返回实现任何。我们只是不想给它命名。这在很多情况下都很方便。

货物检查--quietart[E0308]:`if`和`ali`具有不兼容的类型-->;src/main。RS:21:9|18|/if comfy{19||wolf{}||-由于此原因,预期为20||}否则{21||蜥蜴{}||^需要结构`Wolf`,发现结构`Lizard`22||}||_-`if`和`sel`的类型不兼容

因为即使我们没有指定具体的返回类型(只是它应该实现Any特征),编译器应该仍然能够找出它,给定函数的签名(它的参数类型)和它内部的代码。

现在,它还不能计算出具体的类型应该是struct Wolf还是struct Lizard。

我很好奇为什么编译器不建议使用以下修复程序,我们将重新使用这些修复程序:

Fn Acquisition_PET(comfy:bool)->;Box<;dyn std::any::any>;{if comfy{Box::New(Wolf{})}Else{Box::New(Lizard{})}}。

看,退回狼或蜥蜴的问题在于,这两种动物的大小可能完全不同。

正确的。重点是,我们需要知道实际的类型是什么-它有多大,它有哪些字段,等等。

但是,如果我们返回Box<;dyn any>;,我们只是返回其类型实现Any的avalue的地址。Box<;T>;只是一个指针,我们知道它的大小(32位为4字节,64位为8字节)。

错误[E0599]:在当前作用域-->;src/main.rs:40:9|40|pet.greet();|^`中找不到`std::boxed::box<;dyn std::any::any>;`中的struct`std::boxed::box<;dyn std::any::any>;`

不过,这一次,答案很清楚--我们将返回实现任何的东西的地址。

Fn main(){let comfy=ASK_COMFY_PERFER();let PET=Acquisition_PET(Comfy);println!(";我们有一个{:?}";,pet.type_id());}。

$Cargo Run--安静你喜欢舒适的宠物吗?(是或否)是的,我们有TYPEID{t:13993700938491603631}$货运--安静,你喜欢舒适的宠物吗?(是或否)NOWE获得类型ID{t:8639049246320250335}。

我们可以做的另一件事是尝试将结果值向下转换为特定的具体类型,如Wolf或Lizard:

Fn main(){让comfy=ASK_COMFY_PERFER();让PET=。

.