如何在Julia中创建一个“单分派、面向对象的类”,使其行为类似于具有公共/私有字段和方法的标准Java类

23

我在一本书中读到过这样一句话:“你无法使用单派发方式的方法(例如 obj.myfunc() )在julia中创建传统的'类'”...但我认为这听起来更像是挑战而不是事实。

因此,这里是我的 JavaClass 类型,拥有公共/私有字段和方法,仅为了纯粹的震惊和恐怖因素,就像在Julia中有这样丑陋的东西一样,尽管开发人员们已经尽力避免它:

type JavaClass

    # Public fields
    name::String

    # Public methods
    getName::Function
    setName::Function
    getX::Function
    getY::Function
    setX::Function
    setY::Function

    # Primary Constructor - "through Whom all things were made."
    function JavaClass(namearg::String, xarg::Int64, yarg::Int64)

        # Private fields - implemented as "closed" variables
        x = xarg
        y = yarg

        # Private methods used for "overloading"
        setY(yarg::Int64) = (y = yarg; return nothing)
        setY(yarg::Float64) = (y = Int64(yarg * 1000); return nothing)

        # Construct object
        this = new()
        this.name = namearg
        this.getName = () -> this.name
        this.setName = (name::String) -> (this.name = name; return nothing)
        this.getX = () -> x
        this.getY = () -> y
        this.setX = (xarg::Int64) -> (x = xarg; return nothing)
        this.setY = (yarg) -> setY(yarg) #Select appropriate overloaded method

        # Return constructed object
        return this
    end

    # a secondary (inner) constructor
    JavaClass(namearg::String) = JavaClass(namearg, 0,0)
end

使用示例:

julia> a = JavaClass("John", 10, 20);

julia> a.name # public
"John"

julia> a.name = "Jim";

julia> a.getName()
"Jim"

julia> a.setName("Jack")

julia> a.getName()
"Jack"

julia> a.x # private, cannot access
ERROR: type JavaClass has no field x

julia> a.getX()
10

julia> a.setX(11)

julia> a.getX()
11

julia> a.setY(2) # "single-dispatch" call to Int overloaded method

julia> a.getY()
2

julia> a.setY(2.0)

julia> a.getY()  # "single-dispatch" call to Float overloaded method
2000

julia> b = JavaClass("Jill"); # secondary constructor

julia> b.getX()
0

本质上,构造函数成为了一个闭包,这就是如何创建“私有”字段和方法/重载的方式。
除了“天啊,为什么??为什么要这么做?”之外,还有什么想法吗?
还有其他方法吗?
您能想象会出现哪些可能会惨败的情况吗?


2
我不知道这是否适合在StackOverflow上发布...但是它很有趣。 - Chris Rackauckas
1
你只是想分享这个...但我的意思是,我也想要。 - Chris Rackauckas
7
我相信会有人在stackoverflow上寻找这个确切的问题,这是一种不可避免的问题类型!因此,虽然格式可能有点不正规,但对于stackoverflow来说,这可能是一个合法的文章 :) - Tasos Papastylianou
4
我很佩服你为了避免写论文所做的努力! - Michael Ohlrogge
2个回答

42

当然,这并不是在Julia中创建对象和方法的惯用方式,但也没有什么特别糟糕的地方。在任何具有闭包的语言中,您都可以像这样定义自己的“对象系统”,例如查看在Scheme内已开发的许多对象系统。

在Julia v0.5中,由于闭包自动将其捕获的变量表示为对象字段,因此有一种特别简洁的方法来实现这一点。例如:

julia> function Person(name, age)
        getName() = name
        getAge() = age
        getOlder() = (age+=1)
        ()->(getName;getAge;getOlder)
       end
Person (generic function with 1 method)

julia> o = Person("bob", 26)
(::#3) (generic function with 1 method)

julia> o.getName()
"bob"

julia> o.getAge()
26

julia> o.getOlder()
27

julia> o.getAge()
27

你需要返回一个函数来实现这个功能,这听起来很奇怪,但它确实有好处。它可以从许多优化中受益,例如语言可以为你找出精确的字段类型,因此在某些情况下,我们甚至可以内联这些"方法调用"。另一个很酷的特性是函数的最后一行控制哪些字段是"公共的";列在那里的任何东西都将成为对象的字段。在这种情况下,你只能获取到方法,而不能获取到姓名和年龄变量。但如果你在列表中添加了name,那么你也可以像这样使用o.name。当然这些方法也是多方法的;你可以为getOlder等方法添加多个定义,它会按照你的预期工作。


2
非常有趣!特别是关于捕获变量变成字段的那一部分!然而,我发现这种方法存在一个问题:这些字段似乎是不可变的,这违背了“public”字段的目的;此外,如果存在“setter”方法,它们的值似乎会变成“boxed”,导致进一步的复杂性(例如,如果将age添加到上面的“public”变量中,则o.age + 1会失败并显示ERROR: MethodError: no method matching +(::Core.Box, ::Int64))。在这种方法中是否有任何解决这些问题的方法? - Tasos Papastylianou
2
我确实喜欢这个事实,因为它可以导致一个潜在的“可调用”对象(即,如果复合语句中的最后一个参数本身就是一个函数)!这几乎就像创建了一个“对象”,该对象还具有重载的()运算符,就像C++中的Functors一样。 - Tasos Papastylianou
4
很荣幸与造物主见面。(原谅我,主啊!我不配!) - Tasos Papastylianou

2

Jeff Bezanson的答案很好,但是如评论中所提到的,字段可能会被装箱,这非常令人烦恼。

有一个更好的解决方案来解决这个问题。

备选方案1(基本上与问题中提出的解决方案相同):

# Julia
mutable struct ExampleClass
    field_0
    field_1
    method_0
    method_1
    method_2

    function ExampleClass(field_0, field_1)
        this = new()

        this.field_0 = field_0
        this.field_1 = field_1

        this.method_0 = function()
            return this.field_0 * this.field_1
        end

        this.method_1 = function(n)
            return (this.field_0 + this.field_1) * n
        end

        this.method_2 = function(val_0, val_1)
            this.field_0 = val_0
            this.field_1 = val_1
        end

        return this
    end
end

ex = ExampleClass(10, 11)
ex.method_0()
ex.method_1(1)
ex.method_2(20, 22)

备选方案2:

mutable struct ExampleClass
    field_0
    field_1

    function ExampleClass(field_0, field_1)
        this = new()

        this.field_0 = field_0
        this.field_1 = field_1

        return this
    end
end

function Base.getproperty(this::ExampleClass, s::Symbol)
    if s == :method_0
        function()
            return this.field_0 * this.field_1
        end
    elseif s == :method_1
        function(n)
            return (this.field_0 + this.field_1) * n
        end
    elseif s == :method_2
        function(val_0, val_1)
            this.field_0 = val_0
            this.field_1 = val_1
        end
    else
        getfield(this, s)
    end
end

ex = ExampleClass(10, 11)
ex.method_0()
ex.method_1(1)
ex.method_2(20, 22)

虽然看起来备选方案1更好,但备选方案2表现更好。

我对此问题进行了更深入的分析,您可以在此处查看:https://acmion.com/blog/programming/2021-05-29-julia-oop/


有趣的替代方法。+1。谢谢分享!(我注意到除了原问题中一些过时的语法之外,你的第一种方法与问题中的方法相同,只是没有私有字段/方法 - 我不知道这是否是有意的!) - Tasos Papastylianou
是的,你说的第一个选择是正确的。我没有意识到被弃用的关键字"type"与"mutable struct"是相同的。我会在答案中添加注释。 - Acmion

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