一个只能通过使用最终类来满足的Swift协议要求

24

我正在Swift上建模所有者/被拥有者方案:

class Owner<T: Ownee> {
     // ...
}

protocol Ownee {
    var owner: Owner<Self> { get }
}

然后我有一对遵循上述建模类型的教授/学生类:

class Professor: Owner<Student> {
    // ...
}

class Student: Ownee {
    let professor: Professor
    var owner: Owner<Student> {  // error here (see below)
        return professor
    }

    init(professor: Professor) {
        self.professor = professor
    }
}

然而在Student类中定义var owner时出现以下错误:

因为它在非参数、非结果类型位置使用'Self',所以协议'Ownee'的要求'owner'不能由非终极类('Student')满足。

我试图理解这个错误的原因,为什么将类Student设为final可以修复它,并且是否有一些变通方法来以不同的方式对其进行建模,而不必使此类成为final。 我已经谷歌了这个错误,但到目前为止没有找到太多信息。


1
如果将 Student 子类化会发生什么?owner 属性仍然是 Owner<Student>,但是 Student != StudentSubclass - Alexander
1
@AMomchilov 这是真的,但这有问题吗?如果有,为什么? - luk2302
1
是的,因为Student符合Ownee协议,其类型约束规定owner必须是一个Owner<Self>,其中Self指代符合条件的类型Student。子类化将违反此协议,因此不允许,编译器建议您将Student声明为final。 - Alexander
4个回答

19
错误是正确的。您需要使您的类为final,因为没有子类可以符合您的协议Ownee
考虑这个子类:
class FirstGradeStudent: Student {
   // Student contains following variable:
   // var owner: Owner<Student> {
   //     return professor
   //  }
}

如您所见,由于其父类,它必须实现var owner: Owner<Student>,但实际上应该实现var owner: Owner<FirstGradeStudent>,因为协议包含var owner: Owner<Self> { get },而在这种情况下Self将是FirstGradeStudent
解决方法:
1:为Ownee定义一个超类,它应该被Owner使用:
class Owner<T: OwneeSuper> {
    // ...
}

protocol OwneeSuper {}    
protocol Ownee: OwneeSuper {
    associatedtype T: OwneeSuper
    var owner: Owner<T> { get }
}

OwneeSuper只是一个解决方案,用来克服这个问题,否则我们将只使用:

protocol Ownee {
    associatedtype T: Ownee
    var owner: Owner<T> { get }
}

2. 对于符合 Ownee 协议的类,您必须通过定义 typealias 来将抽象类型的 associatedtype 转换为具体的类:

class Student: Ownee {
    typealias T = Student // <<-- define the property to be Owner<Student>
    let professor: Professor
    var owner: Owner<T> { 
        return professor
    }

    init(professor: Professor) {
        self.professor = professor
    }
}

3. 子类现在可以使用该属性,该属性将是您定义的类型:

class FirstGradeStudent: Student {
    func checkOwnerType() {
        if self.owner is Owner<Student> { //warning: 'is' test is always true
            print("yeah!")
        }
    }
}

谢谢您的回答。我想知道是否有一种方法可以模拟我想要的东西。我希望Ownee对象具有对其所属的Owner实例的引用。我的意图是,一旦在Student中定义,所有者将始终是Owner <Student>类型,这显然可以是Student实例的所有者,也可以是Student子类的实例的所有者。我期望FirstGradeStudent().owner也是Owner<Student>类型。 - Ernesto

10
以下语法应该支持您所需的内容:
protocol Ownee {
    associatedtype Owned = Self where Owned:Ownee
    var owner: Owner<Owned> { get }
}

(在使用Swift 4 - Beta 1进行测试)


这应该是被接受的答案。 - Míng
我觉得这可以更简短一些:associatedtype Owned: Ownee。但是在我测试的过程中,似乎没有一个版本强制要求Self作为关联类型。这意味着我可以有两个类class A: Owneeclass B: Ownee,而A将有var owned: Owner<A>,而B可以有完全相同的代码行,但不会有var owned: Owner<B>,并且不会引发任何编译错误。我希望在这种情况下associatedtype Owned = Self能够实现这个目标 - 强制要求Self。我是否漏掉了什么? - ramzesenok

7
如果Student被子类化了会发生什么?所有者属性仍然是Owner<Student>,但Student != StudentSubclass
通过使您的Student类符合Ownee协议,您必须满足协议的契约。 Ownee协议规定,Owner具有类型约束,因此Owner泛型类型是符合Ownee(在本例中为Student)的类型。
如果编译器允许子类化(即允许您不将Student定义为final),那么可能存在一个StudentSubclass。这样的子类将继承类型为Owner<Student>Owner属性,但StudentStudentSubclass不同。 Ownee协议的契约被违反,因此不能允许这样的子类存在。

好的,我明白了。但是有没有一种方法来模拟我想要模拟的内容?我原本以为一旦owner属性被Student满足,那么从它继承的类就会直接使用它,并且StudentSubclass(professor: p).owner === p会成立。 - Ernesto
1
你可以将类型约束更改为 T: Self,以便符合的类型的子类变得可以接受。当 ownerStudent 满足时,它必须是一个 Owner<Student> 类型。没有子类可以存在而不破坏 Owner 的泛型类型与符合类型相同的要求。 - Alexander
我实际尝试过,显然它不支持该上下文中的那种符号表示法。它会在Owner<T: Self>:所在的位置,给出错误提示“预期使用'>'来完成通用参数列表”。 - Ernesto
1
类型约束(“<T: Self>”)放在协议声明中,而只有通用类型(“<T>”)在Owner的定义中使用。以下是一个示例,展示了它应该是什么样子:https://dev59.com/XGAf5IYBdhLWcg3w_Gyr - Alexander
1
@Ernesto 是的,你说得对,协议不支持泛型类型,只支持关联类型。 - Daniel
显示剩余4条评论

5
如果您来到这个帖子,只是想找到一种让子类继承一个引用自身类型(Self的类型)方法的方法,那么一个解决方法是将父类参数化为子类的类型。例如:
class Foo<T> {
    func configure(_ block: @escaping (T)->Void) { }
}

class Bar: Foo<Bar> { }

Bar().configure { $0... }

但是...似乎你可以在协议扩展中添加一个方法,而该方法不会被认为符合协议本身。我不完全理解为什么会这样运作:
protocol Configurable {
    // Cannot include the method in the protocol definition
    //func configure(_ block: @escaping (Self)->Void)
}

public extension Configurable {
    func configure(_ block: @escaping (Self)->Void) { }
}

class Foo: Configurable { }

class Bar: Foo { }

Bar().configure { $0... }

如果您取消注释协议定义中的方法,您将会得到编译错误:无法通过非最终类 ('Foo') 使用 'Self' 在非参数、非结果类型位置上满足协议“Configurable”的要求“configure”


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