Julia结构体中的可变字段

21

在stackoverflow和Julia文档中我都找不到以下“设计问题”的答案:

假设我想定义以下对象:

struct Person
birthplace::String
age::Int
end

由于 Person 是不可变的,我很高兴没有人可以更改任何创建的 Person 的出生地,然而,这也意味着随着时间的推移,我也不能更改他们的 age

另一方面,如果我定义类型 Person 如下:

mutable struct Person
birthplace::String
age::Int
end

我现在可以让它们年龄增加,但是我不再拥有之前在出生地上的安全性,任何人都可以访问并更改它。

目前我找到的解决方法如下:

struct Person
birthplace::String
age::Vector{Int}
end

显然age是一个1元素的Vector。我认为这种解决方案相当丑陋,明显不够优化,因为我每次都要用方括号访问年龄。

是否有其他更优雅的方法在对象中同时拥有不可变和可变字段?

也许问题在于我没有意识到在struct中将所有内容都设置为可变或不可变的真正价值。如果是这样,请您给我解释一下。


从你提问的方式来看,我猜你并不需要一个incrementage函数来创建一个带有正确年龄的新对象?例如:incrementage(p::Person) = Person(p.birthplace, p.age+1); - Tasos Papastylianou
确切地说,我不想创建一个新对象。 这个想法是创建一个对象,从Web获取一些信息并更新其中一些字段。 由于它每隔大约10秒钟轮询一次,因此我不想每次都创建一个新对象。 但还是谢谢! - stefabat
你可以选择使用定制的内部构造函数,完全控制可见性和可变性,就像这里所示:https://dev59.com/PVkT5IYBdhLWcg3wD7Zb(免责声明:自己问题的无耻宣传) - Tasos Papastylianou
不错!我实际上是从C++转来的,因此我习惯于那种“类型”的对象。 但这绝对不符合该语言的哲学,因此我不会走这条路。 - stefabat
3个回答

21

对于这个特定的例子,最好存储出生日期而不是年龄,因为出生日期也是不可变的,从那个信息计算年龄也很简单。但也许这只是一个玩具示例。


我认为这种解决方案非常丑陋,绝对不是最佳选择,因为每次都要用方括号访问年龄。

通常你会定义一个getter,比如age(p::Person) = p.age[1],你可以使用它来代替直接访问字段。这样就避免了用方括号带来的“丑陋”。

在这种情况下,我们只想存储一个单一值,还可以使用Ref(或可能是0维的Array),例如:

struct Person
    birthplace::String
    age::Base.RefValue{Int}
end
Person(b::String, age::Int) = Person(b, Ref(age))
age(p::Person) = p.age[]

使用方法:

julia> p = Person("earth", 20)
Person("earth", 20)

julia> age(p)
20

确实,这只是一个玩具示例... 一般来说,我倾向于仅将“get”函数用于对最终用户可访问的字段,而在代码深处(用户永远不必查看的地方)直接访问字段。 此外,我不知道直接访问字段和通过函数访问之间是否存在性能差异。 - stefabat
1
你也可以仅为内部使用编写getter方法,而且性能没有任何差异。 - fredrikekre
1
没有性能差异,因为一个类型稳定的小函数将会被内联,所以编译器实际上会消除函数调用并粘贴字段访问,使得getter成为高级零成本抽象。 - Chris Rackauckas
3
与其使用 Array{Int,0},使用 Ref 更好。至少在我上次检查时存在性能差异。 - Chris Rackauckas

9

你已经收到了一些有趣的答案,对于“玩具例子”的情况,我喜欢存储出生日期的解决方案。但是对于更一般的情况,我可以想到另一种可能有用的方法。将 Age 定义为自己的可变结构体,将 Person 定义为不可变结构体。即:

julia> mutable struct Age ; age::Int ; end

julia> struct Person ; birthplace::String ; age::Age ; end

julia> x = Person("Sydney", Age(10))
Person("Sydney", Age(10))

julia> x.age.age = 11
11

julia> x
Person("Sydney", Age(11))

julia> x.birthplace = "Melbourne"
ERROR: type Person is immutable

julia> x.age = Age(12)
ERROR: type Person is immutable

请注意,我无法修改Person的任何字段,但可以通过直接访问可变结构体Age中的age字段来修改年龄。您可以为此定义一个访问函数,例如:
set_age!(x::Person, newage::Int) = (x.age.age = newage)

julia> set_age!(x, 12)
12

julia> x
Person("Sydney", Age(12))

另一个答案中讨论的Vector解决方案没有问题。它基本上完成了相同的事情,因为数组元素是可变的。但我认为上面的解决方案更简洁。


1
我本来也想提出类似的建议,但归根结底它仍然相当不美观。可以为 Person 创建一个内部构造函数,该构造函数接受 Int 而不是 Age,这样至少可以执行 Person("Melbourne", 11)。此外,您还可以使 Age 类型可调用,这样您至少可以执行 p.age() 而不是 p.age.age... 但归根结底,除非进行转换(这可能不值得努力),否则它不会在 Int 表达式中无缝使用。再说了,"向量1"方法也不是。 - Tasos Papastylianou
@TasosPapastylianou 是的,同意它看起来仍然有点混乱,但如果标准是可变和不可变对象的组合,我真的看不到更清晰的方法。至少,正如你所说,大部分丑陋可以通过一些直观的访问器函数隐藏起来。如果您的对象有很多字段,您甚至可以进行元编程以避免代码膨胀。 - Colin T Bowers
我仍然更喜欢使用VectorRef选项,因为我不必定义一个新的结构体。特别是考虑到我只是将struct Age创建为struct Person中的一个字段,并且在其他地方没有使用它,这对我来说似乎有点愚蠢。 目前,由于通常在我创建的结构体中有多个我想要可变的字段,我尝试以逻辑方式将它们组合在容器中(通常是Dict)。 - stefabat
4
@Batta 是的,这确实取决于个人喜好。就我个人而言,我很乐意为一次性使用情况创建一个 struct。有时,这种行为对于类型检查非常有用。例如,在金融领域,您可以将价格和交易量都制定为 Float64 类型,或者它们可以是 PriceVolume 类型(只是 Float64 的包装器),但现在,如果您的函数期望 PriceVolume,您将永远不会在代码中意外混淆两者。在生产级别的代码中非常有用,因为像这样的错误会导致巨额损失 :-) - Colin T Bowers
@ColinTBowers,你上一条评论是我这周读到的关于类型最有启发性的事情。我不知道为什么以前没有注意到它。除了“Price”与“Quantity”之外,你还可以使用“kg<:Quantity”与“ton<:Quantity”或者“Dollar:<Price”与“Euro:<Price”,甚至是“Endogenous”与“Exogenous”等等,对吧?太棒了! - PatrickT
1
@PatrickT 当然。我在很大程度上与股权数据一起工作。我自己代码库中存在的一些内容包括:abstract type Currency ; endstruct AUD <: Currency ; end等,或者 abstract type Exchange ; endstruct NYSE <: Exchange ; endstruct ASX <: Exchange ; end等。例如,当涉及到时间时,交易所变得非常有用,因为所有交易所都有不同的营业时间,所以我可以编写返回市场开放和关闭时间并基于不同交易所派遣的函数。 - Colin T Bowers

6

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接