正如我在本博客的其他帖子中提到的(这里肯定很多人都会同意),我坚信设计程序的一个很好的开始是找出人们试图解决的现实问题的良好表示形式,即定义适当的类型。定义类型的一种有用的技术是所谓的SUM类型。有趣的是,它们在不同的编程语言中的用法完全不同,我想在本文中以Rust和Julia为例简要说明这一点。
备注:Julia和Rust是我最喜欢的编程语言,它们特别适合演示我的观点。但是,如果您不使用这两种语言,这不应该阻止您阅读!首先,这两种语言都有相当直观的语法,并且这里显示的代码非常基本,所以您应该能够跟上。第二,您可以用继承的语言(例如Java)代替Julia,或者用您的标记并集语言代替Rust(例如Haskell)。即使不是这样,您也可能会学到一些东西!
假设您正在创建一个星际游戏,其中玩家可以居住在三个不同的恒星系统中:太阳(太阳人)、北极星(北极星)和半人马座阿尔法星(半人马座)。显而易见的第一步是为这三个类别中的每一个定义类型:
(当然,您可以向这些结构中添加一些字段,但这不是这里的重点。)。
现在,假设您需要将玩家值存储在另一个对象(如游戏状态)中:
Homestar(Player::Solarian)=";Sun";Homestar(Player::Polarian)=";Homestar(Player::Centaurian)=";Alpha Centauri&34;Homestar(Player::Player)=";UNKNOWN";#或`ERROR(";UNKNOWN Player class";)`。
Rust没有子类型,但是您可以通过特征(类似于其他语言中的接口)定义类型之间的关系。
但是GameState结构怎么办呢?我们可以利用特征对象,我们只知道它们实现了特定的Trait。我们特别不知道对象的大小,所以我们需要引用它。如果您熟悉Rust,您就知道这要么意味着使用“普通”引用(&;或&;mut)和处理生命周期注释(老实说,不是我最喜欢的),要么使用盒值。如果您不熟悉这一点,无论如何在这里都无关紧要。GameState可能如下所示:
这是可行的,但确实限制了我们对Player特性所能做的事情,因为它需要是对象安全的。例如,问候任意类的另一位Player。
Greet(Self::Polarian,Other::Player)=println(";Hi!&34;)Greet(Self::Polarian,Other::Solarian)=println(";您的星星不是由三颗独立的星星组成吗?可悲!";)。
Julia和Rust都提供了使用类型参数定义泛型类型。此外,还可以对这些参数进行约束。这允许我们像策略1那样做一些简单的事情,但不会在GameState结构中隐藏具体的播放器类型:
特征玩家{...}struct Solarian;实施Solarian玩家{...}struct GameState<;P:玩家>;{白天:U64,玩家:p}。
通常可以认为这种方法的性能更好,因为编译器可以发出具体类型(单形化)的代码,而不是携带跟踪子类型和虚拟方法表的笨拙的超级类型。(据我所知,Julia中的类型player和Rust中的类型Box<;dyn player>;的实例工作起来非常相似。)。
然而,这种方法有一个主要问题:抽象泄漏。GameState结构的用户现在必须决定为参数P选择哪种具体类型,这在Rust中非常严格,在Julia中不是很严格。这可能是可以的,但通常足够多的GameState是处理该问题的正确位置,并且确实应该隐藏该细节。
因此,经验法则是:在代码中“深入”考虑此问题的泛型,但不要错过需要将其保留在内部的抽象级别。
到目前为止,对于Rust来说,这些方法并不真正令人满意。但是枚举来了!枚举(或枚举)是由具体而显式的可能值列表(所谓的变体)组成的类型。
在Rust中,这些值甚至可以拥有自己的数据,这些数据非常适合我们的情况:
枚举PlayerClass{SOL(Solarian),Pol(Polarian),Cent(Centaurian)}struct GameState{DAYTIME:U64,Player:PlayerClass}Impll PlayerClass{FN HOSTAR(&;Self)->;string{Use PlayerClass::*;Match Self{SOL(_)=>;String:From(";Sun";),...}}FN问候语(&。Match(Self,Other){(Cent(_),Pol(_))=>;println!(";您好,三星同行!";),...};}}。
这确实解决了我们以前的所有问题,因此是解决这种情况的首选模式。
存在,并且它将为您提供一个正好具有这三个可能值的PlayerClass类型,但是枚举不是一等公民。(不过,查看@acroexpand@enum Foo A B C,看看枚举在Julia中是如何“工作”是非常有趣的。)但这与我们甚至没有关系,更重要的是,Julia中的枚举变体不能携带额外的数据!因此,Solarian、Polarian和Centaurian结构字段中的所有信息都不能构建到Playaurian结构中,这一点甚至与我们无关,更重要的是,Julia中的枚举变体不能携带额外的数据!因此,Solarian、Polarian和Centaurian结构字段中的所有信息都不能构建到Playaurian结构中。
实施PlayerClass{fn Take Damage(&;mut self){Match&;mut self{PlayerClass::SOL(Solarian)=>;solarian.Health-=42,...};}}。
您现在可能已经猜到了,但是一般的建议是:在Julia中使用子类型,在Rust中使用子类型。但是有一个警告:枚举不能从“外部”扩展。如果您计划让库用户添加新的类型变体,则必须使用某种特征方法。
如果您对Rust或Julia比较熟悉,那么本文的很大一部分内容可能对您来说非常明显:“当然我使用子类型,这就是每个人都喜欢Julia的原因!”或者“我当然使用枚举,它们是Rust的关键特性之一!”你可能会说。
如果您是Julia开发人员(不失一般性),了解Rust中的相应模式可能会帮助您更快地设计合适的类型(如果您遇到这种情况,反之亦然),但我发现这两种模式看起来相当不同,可以在相同的情况下使用,如果您是一名Julia开发人员,了解Rust中的相应模式可能会帮助您更快地设计合适的类型;反之亦然。