次构造语法 Kotlin

9
我有一个包含主构造函数的Kotlin类,代码如下:
class Person(first: String, last: String, age: Int){ 

    init{
        println("Initializing")
    }

}

我想添加一个次要构造函数,将全名解析为firstlast名称,并调用主构造函数。但是,我无法正确使用语法...

class Person(first: String, last: String, age: Int){  

    // Secondary constructor
    constructor(fullname: String, age: Int):
        this("first", "last", age)
        {
            println("In secondary constructor")
        }

    init{
        println("Initializing")
    }
}

这很好运作,因为我实际上没有在次要构造函数中解析fullname。当我继续尝试解析fullname时,会出现问题。
constructor(fullname: String, age: Int):
var first = fullname.split()[0];
...
{
    println("In secondary constructor")
}

我遇到了一个未解决的引用错误:fullname。它在作用域中不存在,但如果我将它放在大括号中,那么我就无法通过this调用主构造函数。
constructor(fullname: String, age: Int):
{
    var first = fullname
    this(first, "foo", age)
    println("In secondary constructor")
}

我遇到了一个涉及缺少invoke函数的错误。

很抱歉,在Kotlin文档中找不到这种情况的好例子。


你可以暴露工厂方法并委托它们,而不是使用构造函数,这样可以让你决定何时实际委托,或者在第二个构造函数中不声明firstlast变量。但如果您不介意我问一下,为什么必须同时公开Person(first, last, age)Person(full name, age)?如果客户端在使用fullName时忘记在firstlast之间添加空格会怎么样?在构造函数委托之前无法声明变量。 - Dioxin
这只是一个玩具示例,我实际上不会构建这两个构造函数。你是说我不能以这种方式使用辅助构造函数吗? 在普通的Java中,我认为你可以在辅助构造函数中声明变量?所以我想这只是一个糟糕的例子,它触及了好的编码应该避免的用例? - Adam Hughes
是的,你不能这样使用次要构造函数。this是一个委托,这就是为什么你不能在大括号内部使用它的原因。你需要做一些类似于constructor(...) : this(fullName.split(" ")[0], fullName.split(" ")[1])的事情,这有可能会超出索引范围。 - Dioxin
那么,如果我的使用情况是“fullName”需要进行非常复杂的验证,我会陷入困境吗? - Adam Hughes
确切地说,你为什么要在首次暴露constructor(fullName)呢?我能想到的唯一情况是从IO读取,但如果是这种情况,解析IO数据的系统应该分割名字的第一个和最后一个部分。名字应该在到达“Person”时被分割,这不是“Person”类的责任,实现这个功能实际上可能会损害可重用性(如果你有一个由两个单词组成的姓氏怎么办?如果吹奏大号的约翰·范·豪滕(John Van Houten)使用该构造函数会有问题,假设他不想让“Van”成为他的姓氏,也不想将其排除在外)。 - Dioxin
2个回答

3

当我需要一个次要构造函数在传递结果给主构造函数之前进行一些计算时,我使用的解决方案是在伴生对象上定义一个函数。实现这个功能的代码如下:

class Person(first: String, last: String, age: Int) {  

    companion object {
        fun fromFullNameAndAge(fullname: String, age: Int) : Person {
          println("In secondary constructor")
          var bits = fullname.split()
          // Additional error checking can (and should) go in here.
          return Person(bits[0],bits[1],age)
        }
    }

    init{
        println("Initializing")
    }
}

您可以像这样使用它。
var p = Person.fromFullNameAndAge("John Doe", 27)

这可能没有 Person("John Doe", 27) 那么整洁,但我认为还不错。

好的,谢谢Michael。这有些让我想起Python。我记得创建过既是构造函数又像静态方法一样被调用的方法。 - Adam Hughes
接下来,你会发现你的API被静态工厂方法淹没了,这些方法执行角落案例检查。如果你想后来添加社保号码,或者可能更改名称解析方式呢?你将不得不违反一些非常有用的原则,比如开放/封闭。 - Dioxin
@VinceEmigh 通常构造函数/工厂函数正是你想要进行边角情况检查的地方,比在代码中的十几个地方散布这些检查要好得多。但是,像往常一样,这是一个平衡问题。我反对盲目为每种情况创建工厂函数,但如果这是一个经常出现的模式,那么肯定可以为其提供支持。 - Michael Anderson
@MichaelAnderson,这不会在代码中到处都是,而是由负责它的对象处理(在这种情况下,是解析器)。这个工厂将解析规则嵌入到“Person”的契约中,损害了它的可重用性。如果他们想公开一个Person(title, first, last, age),现在你需要一个fromFullNameAndAgeAndTitle - Dioxin

1

构造函数通过this调用必须是第一个调用。这就是为什么它被视为委托而不是普通方法调用的原因。这意味着在委托调用之前不能声明变量。

您可以通过内联计划存储在变量中的任何值来解决此问题:

constructor(fullName : String, age : int) : this(fullName.split(" ")[0], fullName.split(" ")[1])

但是,如果没有指定姓氏,或者客户端决定使用“-”或其他字符作为分隔符,这可能会导致索引越界。此外,它也很难看。

设计分析

你的结构问题在于将确定名字的责任交给了Person类。这会降低该类的可重用性,因为它将被限制在一种解析形式上。这就是为什么名字的解析不应由Person执行的原因。

相反,您应该公开主要构造函数,然后让Person的客户端分离名字的姓和名。

解决方案示例

假设我们正在从文件中读取姓名。文件中的每一行都包含一个完整的姓名。

nameFile.forEachLine({ personList.add(Person(it)) })

这是您试图为客户提供的奢侈品:让他们只需输入一个名称,而不必担心解析它的问题。
问题在于缺乏安全性:如果该行仅包含名字怎么办?如果文件没有使用空格来分隔名字和姓氏呢?您将被迫定义新的Person类型,以处理不同的名字和姓氏组合。
相反,解析应该发生在类外部:
file.forEachLine({
    val firstName = ...
    val secondName = ...

    personList.add(Person(firstName, secondName))
})

现在责任已经从Person中移除,如果我们想的话,我们可以将责任交给一个新对象:
val parser = NameParser(" ") //specify delimiter
file.forEachLine({
    val firstName = parser.extractFirstName(it)
    val lastName = parser.extractLastName(it)

    personList.add(Person(firsrName, lastName))
})

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