锈病型系统中的未开发潜力

2021-06-14 17:16:11

今天,I' M写关于除了检查代码属性之外的类型的类型。它会涉及一个好的动态打字,是的,' s在rust.中有一些狂野的想法,所以系好安全带并准备好乘车!

该文章分为介绍,背景,包含主要内容的三个部分,以及结论。中间的三个部分每个都覆盖自己的想法,单独的动机。连接是运行时类型评估的方式。在那个方面,它们彼此的顶部构建。

类型是一个非常抽象的概念。他们甚至是什么?对我来说,答案取决于编程语言和讨论的一般背景。

当我在C ++中写下我的第一行的程序代码时,一个类型只是为了我来定义变量的东西。我有更多的练习,用C ++和Java,我的思想中的类型与类或基元相同。但无论如何,我没有考虑大量的。他们只是让编译器快乐的必需品。

扩展到JavaScript,我意识到类型也可以隐藏在背景中。在这种情况下,他们必须是对运行时开心的权利,这似乎比编译器更宽容。另一方面,我讨厌它只出现在运行时,我知道编译器可以告诉我。

然后,我学会了Haskell。类型成为一个完全不同的概念。它似乎可以在整个程序中写入类型系统本身。我印象深刻。

毕竟,我学会了rust.i被爱了rust.com对C和C ++的感觉有多强烈敏感的一切,Rust删除了来自它们的最令人沮丧的部分。要初始化变黄,不再可能,空指针不再存在,并且内存管理成为爆炸。

快进至今天。 Fort向我展示了几种完全新的概念,可以通过其巧妙的系统来实现.Lifetimes在类型内纳入了内存管理方面。与& mut和&amp之间的区别。类型定义如果允许别名。在某种程度上,实现未来特征的类型描述了整个有限状态机。

但今天我想谈谈Rust时的运行时类型评估。我遇到了一些实际的编程问题,我无法在这里和那里没有一些(安全)沮丧。然后将它带到极端的动态键入我没想到的是。就越可能,我不得不重新考虑一次类型的类型。自从我发现结果非常有趣且令人惊讶,我想在这篇文章中分享它。

在某些语言中,每个(非原始)值的类型都嵌入到机器代码中。它就像在每个对象中隐式存在的隐藏字段。这是启用动态键入的一种方法.But RURT不包括每个rust价值。

但是,RURE提供了手动存储类型信息的方法,该信息也可以在运行时使用。它可能会将静态已知类型的值转换为与一个特征的虚拟功能表(VTable)组合的胖指针。这些脂肪指针称为特质对象。

特质对象基本上提供了选择的运行时类型信息。但它们的权力相当有限,因为它们只能访问特定特征及其父母特征的功能。要知道我们是否正在处理特定类型,需要一个诀窍。

仅使用核心标准库中的工具,我们可以向编译器询问任何类型的类型,并在运行时存储此内容。然后,编译器将为类型ID汇出唯一的常量编号。

使用核心::任何:: {of,typeid}; fn main(){让one_hundred = 100u32; //获取USAing的类型ID该类型的值。让t0 = one_hundred .type_id(); //直接获取类型ID,让T1 = TypeId :: of ::< U32> (); assert_eq! (t0,t1)}

有两个变体显示,一个具有类型的值,另一个具有类型作为通用参数的类型。源代码级别上的函数调用。但是编译器应该优化它们并在他们的位置施加恒定的价值。

然后可以在运行时使用typeId值,因为基本上只有三件事。我们可以将其与另一个类型进行比较,它可以用作哈希键,并且我们可以打印用于调试目的的ID,这只是显示一个随机的调试目的寻找整数值。但是我们不能做其他事情,例如查找特性是否为该类型ID实现了。

以下是类型ID可以用于模拟动态类型检查。 (在操场上运行它!)

fn count_rectangles(形状:& [框< dyn形状>]) - > USIZIZE {让MUT n = 0;出于形状的形状{//需要Derefernce一次或我们将获得盒子的类型! vet type_of_shape = shape .deref().type_id();如果type_of_shape == tyounid :: of ::<矩形> (){n + = 1; } else {println! (" {:?}不是矩形!",type_of_shape); }} n}

方法type_id()是在任何特征上定义的,它具有毯子实现,不出所料,任何类型。 (对类型有很小的限制,但这超出了本文的范围。)

当我们使用dyn的特征对象时,真正的动态键入开始。它可以执行所谓的触觉,从一般类型到更具体的类型。 (参见官方文档的Downcast_Ref和Downcast。)

fn remove_first_rectangle(形状:& mut vec< box<>) - >选择<盒子<矩形>> {让idx = shapes .iter().position(| shoce |形状.deref().type_id()== tyounid :: ef ::< rett; rentangle>())? ;让entrangle_as_unknown_shape = shapes .remove(idx); entrangle_as_unknown_shape .downcast().ok()}

但是,这里的悲观是没有魔法。如果我们想手动实施它(没有编译器的帮助),我们也可以检查类型ID是否与我们的期望匹配,然后用传输呼叫跟进。

但现在足够的背景。让我们在三个概念中获得创造性的概念!

本节展示了像这样的魔法如何在Rust中工作,为什么它很重要。

//将两种不同的类型放在同一收集中,没有键。集合.set(3.14);集合.set(888); //再次取出两种类型的值,//自动获取正确类型Assert_eq的值! (3.14,*集合.get ::< f32>()); assert_eq! (888,*集合.get ::< u32>());

Rust中的大多数集合都是同质的,即它们存储所有相同类型的物体。例如,VEC< f32>只有商店浮动。但是,我们可以通过使用指针来使其单向异构。

例如,vec<框< dyn toString>>存储一系列指针。该矢量可以接受的指针类型包括框< f32>框< u64>以及许多其他类型。以及我们可以放入的数据类型是异系。但我们出去的只是一个指向特质对象的指针(框< dyn toString>),无法恢复内值的实际类型。

要具有完全异因的集合,Getter-方法应该能够返回不同类型的对象。这是以动态类型语言的琐碎的,例如Python或JavaScript.in静态类型的语言,但是,函数只能返回一个特定的类型,如函数签名所定义。

作为一种简单的方法,具有亚型的语言通常具有最常见的类型,这是所有其他类型的类型。例如,java中的对象是所有类的超级类型。这可以在函数签名中定义来定义返回类型。然后调用者可以在返回的值上执行触觉。

在Rust中,任何类型的特征对象都可以被认为是最常见的类型。它是唯一的类型(几乎)所有其他类型可以被胁迫。如在背景部分中所解释的,任何也是(仅限)允许衰退的特质.thus,我们可以返回& box< dyn任何>在getter方法中,呼叫者可以沮丧。

返回框< dyn任何>但直接不是一个很好的界面。要避免在呼叫者侧上的手动衰减,它可以隐藏在通用功能后面。它是一个完整的例子。 (游乐场链接)

使用核心::任何::*;使用std :: collections :: hashmap; fn main(){让mut collection = hyericoCollection :: default();集合.set(" f32",3.14f32);集合.set(" f64",2.71f64);集合.set("另一个f32",1.618f32);让f32_output = * collection .get ::< F32> (" f32").unwrap(); assert_eq! (3.14,f32_output); }#[派生(默认)] struct heterocollection {data:hashmap<& '静态str,盒子< dyn任何>> ,}} ishichyercollection {fn get< T:'静态> (& self,key:'静态str) - >选项<& T> {ver未知_output:&盒子< dyn任何> = self .data .get(key)? ; Unknown_output.downcast_ref()} fn set< T:'静态> (& mut self,key:'静态str,value:t){self .data .insert(key,box :: new(价值)); }}

上面的代码基本上模拟了Python dictionary.any键可以保存任何类型。呼叫者必须确保键和类型匹配。

这是一个疯狂的想法,我们如何让编译器进行检查?以下是一个执行的实现。 (游乐场链接与示例用法)

使用核心::任何::*;使用std :: collections :: hashmap; struct singletoncollection {data:hashmap< TypeId,Box< dyn任何>> ,}}} ill ingletoncollection {pub fn get< T:任何> (& self) - > & t {self .data [& tyoundid :: ::< T> ()] .downcast_ref().as_ref().unwrap()} pub fn set< T:任何> (& mut self,value:t){self .data .insert(tylyId :: of ::< t>(),box :: new(价值)); }}

通过这种方法,通用类型充当键。将集合限制为每种类型的单个元素。但在许多情况下,这不是一个限制。新类型是便宜的!在下面的片段中展示了低于比较的后。

///在收集之前.set("姓名"," jakob");集合.set("语言","生锈");集合.set("主导手和#34;,Dominanthand ::右);让名称= Collection.get ::<& '静态str> ("姓名");让语言=集合.get ::<& '静态str> ("语言");让Dominant_Hand = Collection.get ::< Dominanthand> ("主导手和#34;); //收集后.set(姓名(" jakob"));集合.set(语言("生锈"));集合.set(Dominanthand :: Right);让name = collection .get ::<名称> ().0;让语言= collection .get ::<语言> ().0;让Dominant_Hand = Collection.get ::< Dominanthand> (); // for完整性:类型定义struct struct名称('静态str);结构语言(&'静态str); enum dominanthand {left,右,两者,既不是,未知,其他,}

唯一的功能差异是,必须在编译时已知类型键,而字符串可以在Runtime确定。尽管如此,那么可以确定。后来第三节我将展示一种绕过这个限制的方法。

在句子上,有一点烦恼,因为必须为每个键定义一个新类型。但是,我认为它不是比维护“魔术字符串”列表更糟糕。无论如何,它们可能会成为单独的常数,也是样板代码的一行。

type-key的好处是编译器可以检查该密钥是否有效,并且存储的值与请求的类型匹配。

现在是时候询问了,我们想要使用单例异构系列吗?也许最常见的用法是在想要管理库用户定义的常规状态的库中。

在这种情况下,这种模式派上友好,因为它允许用户任意存储任何类型的许多对象。而且图书馆可以在不知道类型的情况下管理它们。分析2也将有一些很好的例子。

但值得注意的是,我没有发明这个模式。事实上,它是广泛使用的。我认为我在他们的结构世界中第一次看到它第一次见到紫水晶/碎片。

挖掘挖掘时挖掘本文,我发现Chris Morgan在通用收藏中包裹了这种模式.AT箱子的时间超过了130万次历史下载。我会说分类为广泛使用。

因此,类型可以用作密钥,社区已经这样做。揭开未开发的潜力,让我们看看下一节中的机会。

在本节中,我们将看到基于类型的一些动态调度。不是基于名称和类型的动态调度组合,否,仅根据类型的调度.Additionaly,即使是对象将由其类型动态查找,这意味着呼叫者甚至不需要访问对象!

我将显示的是,您可以被描述为面向对象的消息传递,该扭曲通过该类型用作对象地址以及动态调度。

但是,让我对这里的术语非常清楚。我指的是面向对象编程(OOP)的一般想法,它不需要类。它只是我正在使用的对象和方法。

此外,在此上下文中传递的消息是用于调用对象上的方法的特定术语.Enteally,使用该方法的标识符和参数值的消息发送到对象,对象在内部调度并执行该对象。

去年,我写了关于我面临的问题,通过WASM在浏览器中运行。 (请参阅Rust遇到Web - 一块编程范式的冲突)

要简短,它归结为浏览器中未连续运行的线程。相反,必须在间隔内登记闭包。在那些之间的分析数据可以获得毛茸茸,我在该文章中描述。

下面是一个人工示例,说明浏览器的代码如何使用回调闭环。

fn main(){let window = get_window_from_browser();让body = get_body_from_browser();让州= mydummystate :: new();窗口.set_interval(100,move || {//每100ms状态做某事.Update();});身体.on_click(move | x,y | {//在每次单击状态时执行一些东西.apply_click(x,y);}); }

此示例不编译。 (即使在假设所有函数都存在正确的签名时。)问题是状态在两个封闭件内移动,这不起作用。借用而不是移动将无法运行,因为闭包从主函数使用的当前堆栈帧中开始。

要解决此问题,我必须将数据放在共享智能指针后面,如弧形<>然后介绍内心的可变性。这很烦人,我想有更好的方式。

回来后,当我写的文章抱怨这些问题时,我并没有真正解决这个问题,我刚刚指出了它。但是现在,我认为我有一个令人满意的解决方案,我已经使用了很多几个月。

如前所述,我最终提出的解决方案涉及具有动态方法注册和动态调度的单例存储器上的解决方案。我只是向您展示一些代码,希望它会使事情更清晰。

struct myobject {counter:u32,} struct methoda; struct methodbwitharguments {text:string,} illich myObject {fn method_a(& mut self,_ arg:methoda){self .counter + = 1; println! ("对象调用方法{}次。这个时候没有参数。",自我.counter); fn method_b(& mut self,arg:methodbwitharguments){self .counter + = 1; println! ("对象调用方法{}时间。这次与参数:{}",self .counter,arg .text); fn main(){/ *注册* / let obj = myObject {计数器:0}; my_library :: register_object(obj); my_library :: register_method(myobject :: method_a); my_library :: register_method(myObject :: method_b); / *调用* / my_library :: Invoke ::< myObject,_> (Methoda); my_library ::调用::< myObject,_> (方法编织方法{text:"您好世界!" .to_owned(),}); / *输出* / //对象调用一个方法1次。这次没有争论。 //对象调用方法2次。这次争论:你好世界! }

这里发生了什么发生的是,我将一个对象(obj)及其方法注册到全局托管状态的my_library.After,我正在调用该对象的方法而不实际引用obj.这是可能的,因为my_library它全局存储。

全局存储只能保持每种类型的一个对象。 (内部使用异形单例集合。)因此,只要指定了该类型,也称为应调用的对象。

当使用封闭件作为回调时,这变得非常有用。我们现在可以在共享对象上有许多不同的回调,而无需实际担心数据共享部分。

fn main(){// ... div .on_click(|| {my_library ::调用::< myobject>(methodbwitharguments {test:"点击某些东西!" .to_owned(),} });}

所以,我在一个名为坚果的库中实施了这个(和更多)。该命名在实际库中有点不同。例如,对象称为活动。这只是因为我没有将其视为对象和方法,直到我开始写入这篇文章的第三次尝试。

这么多目标是什么。现在挑战是如何实现My_Library的函数。

要实现我刚刚介绍的界面,我们需要一堆全局状态坐在后台坐在后台,存储对象和方法.Llet不用如何存储和拾取全局状态。要保持专注于动态键入,我们只是假设方法register_object和register_method在螺母对象上调用。游乐场包括如果您想自己运行它,可以使其工作。

在那个假设下,应该进入螺母内容?让我们从集合开始存储对象。

这正是我在第1节中向您展示了您的单例。可以按住不同对象的集合,由其类型索引。

ichill螺母{fn register_object<对象> (& mut self,obj:object)在其中对象:任何,{let key = tyounid :: ::<对象> ();让boxed_obj = box :: new(obj);自我.Objects .insert(key,boxed_obj); }}

它会导致方法。我们需要使用异因类型存储任意数量的方法。将它们存储在一个集合中,我们需要找到一个涵盖它们的一般特征对象。

盒子< dyn任何>会努力存储它们。但是我们稍后需要调用这些方法。这将需要触觉到实际类型。

要诚实地,可以这样做。但是如果我们存储可调用的函数指针,我们就可以让我们的生活更容易。我们只需要找到一般的可调用类型。 首先,我们必须选择其中一个特征Fn,fnonce和fnmut作为我们的基地特征。 FnMut是它们中最普遍的,我们将与之相同,不限制用户。(您可以在FNMUT文档中介绍它们之间的差异,并在umstonomicon章节中究竟在较高级别特质范围内的内容 ) 接下来,W. ......