【问题标题】:mutable fields in Julia structJulia 结构中的可变字段
【发布时间】:2022-03-01 18:11:56
【问题描述】:

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

假设我要定义以下对象

struct Person
birthplace::String
age::Int
end

由于Person 是不可变的,我很高兴没有人可以更改任何已创建的Personbirthplace,尽管如此,这也意味着随着时间的推移,我也无法更改他们的age...

另一方面,如果我将类型 Person 定义为

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

我现在可以将它们设为age,但我没有以前在birthplace 上的安全性,任何人都可以访问并更改它。

目前我发现的解决方法如下

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

显然age 是一个单元素Vector
我发现这个解决方案非常丑陋,而且绝对不是最理想的,因为我每次都必须用方括号访问年龄。

有没有其他更优雅的方式在一个对象中同时拥有不可变和可变字段?

也许问题是我错过了在struct 中让所有东西都可变或不可变的真正价值。如果是这样,你能解释一下吗?

【问题讨论】:

  • 从您提出问题的方式来看,我认为您不关心创建具有正确年龄的新对象的incrementage 函数?例如incrementage(p::Person) = Person(p.birthplace, p.age+1);
  • 没错,我不想创建一个新对象。这个想法是一个从网络上获取一些信息并更新它的一些字段的对象。因为它会每 10 秒左右轮询一次,所以每次都创建一个新对象不是我想要的。但无论如何,谢谢!
  • 可以使用定制的内部构造函数让您完全控制可见/可变的内容,例如:stackoverflow.com/q/39133424/4183191(免责声明:无耻自己的问题的插头)
  • 不错!我实际上来自 C++,因此我习惯了那种“类型”的对象。但这绝对不在语言哲学中,因此我不会走那条路。

标签: struct julia immutability mutable


【解决方案1】:

对于这个特定的示例,存储生日而不是年龄似乎更好,因为生日也是不可变的,并且从该信息计算年龄很简单,但这可能只是一个玩具示例。


我发现这个解决方案非常丑陋,而且肯定不是最理想的,因为我必须这样做 每次使用方括号访问年龄。

通常您会定义一个 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”函数,如您所说的仅用于最终用户可访问的字段,而在代码的深处(用户永远不必看的地方)我直接访问字段。另外,我不知道直接访问字段和通过函数访问之间是否存在性能差异..
  • 您也可以拥有仅供内部使用的 getter,并且没有性能差异。
  • 没有性能差异,因为一个类型稳定的小函数只会内联函数,所以编译器基本上会摆脱函数调用并粘贴在字段访问中,使getter高级零成本抽象。
  • 而不是Array{Int,0},使用Ref 可能会更好。至少在我上次检查时存在性能差异。
【解决方案2】:

您收到了一些有趣的答案,对于“玩具示例”案例,我喜欢存储出生日期的解决方案。但对于更一般的情况,我可以想到另一种可能有用的方法。将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 解决方案没有任何问题。它本质上完成了同样的事情,因为数组元素是可变的。但我认为上面的解决方案更整洁。

【讨论】:

  • 我正打算提出一些类似的建议,但归根结底,它仍然相当难看。可以为Person 创建一个内部构造函数,它接受Int 而不是Age,这样至少你可以做Person("Melbourne", 11)。您也可以使 Age 类型可调用,因此您至少可以使用 p.age() 而不是 p.age.age ...但归根结底,除非有人下线,否则它不会在 Int 表达式中无缝使用转换也是如此(这可能不值得付出努力)。再说一次,“1 的向量”方法也不是。
  • @TasosPapastylianou 是的,同意它仍然看起来有点乱,但如果标准是可变和不可变对象的组合,我真的没有看到更清洁的方法。至少,正如您所说,大多数丑陋都可以通过一些直观的访问器功能隐藏起来。如果您的对象有很多字段,您甚至可以使用元编程来避免代码膨胀。
  • 我仍然更喜欢VectorRef 的选项,因为我不必定义新结构。特别是,考虑到我将创建struct Age 只是为了使其成为struct Person 中的一个字段,而不是在其他任何地方使用它,在我看来有点傻。目前,由于通常在我正在创建的结构中,我希望拥有多个可变字段,因此我尝试以一种合乎逻辑的方式将它们组合在容器中(通常是 Dict)。
  • @Batta 是的,这真的取决于个人喜好。我个人很高兴为一次性用例创建struct。有时这种行为对于类型检查非常有用。例如,在金融领域,您可以将价格和交易量都设为Float64 类型,或者它们可以是PriceVolume 类型(只是Float64 上的包装器),但现在如果您的函数需要PriceVolume,你永远不会在你的代码中不小心混淆这两者。生产级代码的有用技巧,像这样的事故会花费很多钱:-)
  • @PatrickT 哦,当然。我经常处理股票数据。我自己的代码库中存在的一些东西包括:abstract type Currency ; endstruct AUD <: Currency ; end 等等,或者abstract type Exchange ; endstruct NYSE <: Exchange ; endstruct ASX <: Exchange ; end 等等。交易所一在处理时间时变得非常有用,例如,因为所有交易所都有不同的营业时间,所以我可以编写返回市场开盘和收盘时间的函数,并根据不同的交易所调度。
【解决方案3】:

在 Julia 1.8 中,您可以使用

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

参照。 https://docs.julialang.org/en/v1.8-dev/manual/types/#Composite-Types

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多