Функция протокола возвращает Self


82

У меня есть протокол P, который возвращает копию объекта:

protocol P {
    func copy() -> Self
}

и класс C, реализующий P:

class C : P {
    func copy() -> Self {
        return C()
    }
}

Однако, если я помещаю возвращаемое значение, Selfя получаю следующую ошибку:

Невозможно преобразовать возвращаемое выражение типа C в возвращаемый тип Self

Я тоже пытался вернуться C.

class C : P {
    func copy() -> C  {
        return C()
    }
}

Это привело к следующей ошибке:

Метод copy () в неокончательном классе C должен возвращаться, Selfчтобы соответствовать протоколу P

Ничего не работает , за исключением случая , когда я PREfix class Cс , finalт.е. сделать:

final class C : P {
    func copy() -> C  {
        return C()
    }
}

Однако, если я хочу создать подкласс C, ничего не получится. Есть ли способ обойти это?


1
Что вы имеете в виду под «ничего не работает»?
— Роб Нэпир

Компилятор жалуется при вводе C- или Самости в качестве возвращаемого значения , если classне являетсяfinal class
— aeubanks

6
Хорошо, я воспроизвел ошибки, но, задавая вопросы, вы должны указать фактическую ошибку, которая возвращается. Не просто «выдает ошибки» или «не работает».
— Роб Нэпир

Здесь компилятор совершенно прав в своих ошибках, кстати. Я просто думаю о том, сможете ли вы вообще добиться того, что пытаетесь сделать.
— Роб Нэпир

1
Но вы можете позвонить [[[self class] alloc] init]. Итак, я предполагаю, что вопрос в том, существует ли безопасный для типов способ вызвать текущий класс и вызвать метод инициализации?
— Aeubanks 03

Ответы:


144

Проблема в том, что вы даете обещание, которое компилятор не может доказать, что вы его сдержите.

Итак, вы создали это обещание: Вызов copy() вернет свой собственный тип, полностью инициализированный.

Но тогда вы реализовали copy()так:

func copy() -> Self {
    return C()
}

Теперь я подкласс, который не отменяет copy(). И я возвращаю Cне полностью инициализированный Self(что обещал). Так что это нехорошо. Как насчет:

func copy() -> Self {
    return Self()
}

Что ж, это не скомпилируется, но даже если бы это было так, это было бы бесполезно. У подкласса может не быть тривиального конструктора, поэтомуD() может быть даже незаконным. (Хотя см. Ниже.)

Хорошо, а как насчет:

func copy() -> C {
    return C()
}

Да, но это не возвращается Self. Он возвращаетсяC . Ты все еще не сдерживаешь своего обещания.

"Но ObjC может это сделать!" Ну вроде как. В основном потому, что его не волнует, сдержите ли вы свое обещание, как это делает Swift. Если вам не удастся реализовать copyWithZone:в подклассе, вы можете не полностью инициализировать свой объект. Компилятор даже не предупредит вас, что вы это сделали.

«Но почти все в ObjC можно перевести на Swift, а в ObjC есть NSCopying». Да, это так, и вот как это определяется:

func copy() -> AnyObject!

Таким образом, вы можете сделать то же самое (здесь нет причин!):

protocol Copyable {
  func copy() -> AnyObject
}

Это говорит: «Я ничего не обещаю о том, что вы получите в ответ». Вы также можете сказать:

protocol Copyable {
  func copy() -> Copyable
}

Это обещание, которое вы можете дать.

Но мы можем немного подумать о C ++ и вспомнить, что есть обещание, которое мы можем дать. Мы можем пообещать, что мы и все наши подклассы будем реализовывать определенные типы инициализаторов, и Swift будет обеспечивать это (и таким образом может доказать, что мы говорим правду):

protocol Copyable {
  init(copy: Self)
}

class C : Copyable {
  required init(copy: C) {
    // Perform your copying here.
  }
}

И вот как вы должны выполнять копии.

Мы можем сделать еще один шаг вперед, но он используется dynamicType, и я не тестировал его тщательно, чтобы убедиться, что это всегда то, что мы хотим, но он должен быть правильным:

protocol Copyable {
  func copy() -> Self
  init(copy: Self)
}

class C : Copyable {
  func copy() -> Self {
    return self.dynamicType(copy: self)
  }

  required init(copy: C) {
    // Perform your copying here.
  }
}

Здесь мы обещаем, что есть инициализатор, который выполняет копии для нас, а затем мы можем во время выполнения определить, какой из них вызвать, предоставив нам синтаксис метода, который вы искали.


Хм, должно быть, они это изменили. Я мог бы поклясться, что это func copy() -> Cработало в предыдущих бета-версиях, и это было согласованно, потому что соответствие протоколу не передавалось по наследству. (Теперь кажется, что соответствие протокола передается по наследству и func copy() -> Cне работает.)
— newacct 03

2
Последнее решение на чистом Swift не работает с подклассами, так как они должны быть реализованы init(copy: C)вместо этого init(copy: Self):(
— fluidsonic

Последнее решение гарантирует, что возвращаемое значение будет, Selfно все инициализаторы должны принять статически типизированную переменную, то Cесть, это не так уж много улучшений для простого возврата AnyObjectв первую очередь.
— chakrit

1
В swift 2.0 вам нужно было бы явно вызывать init:self.dynamicType.init( ... )
— pronebird

1
@Dschee внутри C, Self может быть C или подклассом C. Это разные типы.
— Роб Напье

25

В Swift 2 мы можем использовать для этого расширения протокола.

protocol Copyable {
    init(copy:Self)
}

extension Copyable {
    func copy() -> Self {
        return Self.init(copy: self)
    }
}

Это отличный ответ, и такой подход широко обсуждался на WWDC 2015.
— gkaimakas

2
Это должен быть принятый ответ. Это можно упростить с помощью return Self(copy: self)(по крайней мере, в Swift 2.2).
— jhrmnn

16

Есть еще один способ сделать то, что вы хотите, - воспользоваться преимуществами ассоциированного типа Swift. Вот простой пример:

public protocol Creatable {

    associatedtype ObjectType = Self

    static func create() -> ObjectType
}

class MyClass {

    // Your class stuff here
}

extension MyClass: Creatable {

    // Define the protocol function to return class type
    static func create() -> MyClass {

         // Create an instance of your class however you want
        return MyClass()
    }
}

let obj = MyClass.create()

Очаровательно. Интересно, относится ли это к stackoverflow.com/q/42041150/294884
— Fattie

Этот делает то, что мне интересно. Спасибо!
— Джош в The Nerdery

10

На самом деле есть трюк, который позволяет легко вернуться, Selfкогда этого требует протокол ( суть ):

/// Cast the argument to the infered function return type.
func autocast<T>(some: Any) -> T? {
    return some as? T
}

protocol Foo {
    static func foo() -> Self
}

class Vehicle: Foo {
    class func foo() -> Self {
        return autocast(Vehicle())!
    }
}

class Tractor: Vehicle {
    override class func foo() -> Self {
        return autocast(Tractor())!
    }
}

func typeName(some: Any) -> String {
    return (some is Any.Type) ? "\(some)" : "\(some.dynamicType)"
}

let vehicle = Vehicle.foo()
let tractor = Tractor.foo()

print(typeName(vehicle)) // Vehicle
print(typeName(tractor)) // Tractor

1
Вау. компилируется. Это хитро, потому что компилятор не позволит вам простоreturn Vehicle() as! Self
— SimplGy 01

это ошеломляет. Вау. То, что я здесь спрашиваю, на самом деле является вариацией этого? stackoverflow.com/q/42041150/294884
— Fattie 04

@JoeBlow Боюсь, что это не так. Я бы сказал, что для обеспечения безопасности мы должны точно знать возвращаемый тип (т.е. не «A или B», а просто «A»; в противном случае мы должны думать о полиморфизме + наследовании + перегрузке функций (как минимум).
— werediver

это обман компилятора. Поскольку переопределение foo()не выполняется принудительно, каждый Vehicleпотомок без foo()специальной реализации вызовет очевидный сбой autocast(). Например: class SuperCar: Vehicle { } let superCar = SuperCar.foo() . Экземпляр Vehicleне может быть понижен до SuperCar- поэтому принудительное развертывание nil в 'autocast ()' приводит к сбою.
— freennnn

1
@freennnn Изменение кода на следующий не приводит к сбою, если подкласс не отменяет foo(). Единственное требование - класс Fooдолжен иметь требуемый инициализатор, чтобы это работало, как показано ниже. class Vehicle: Foo { public required init() { // Some init code here } class func foo() -> Self { return autocast(self.init())! // return autocast(Vehicle())! } } class Tractor: Vehicle { //Override is not necessary /*override class func foo() -> Self { return autocast(Tractor())! }*/ }
— shawnynicole

2

Следуя предложению Роба, это можно сделать более универсальным для связанных типов . Я немного изменил пример, чтобы продемонстрировать преимущества подхода.

protocol Copyable: NSCopying {
    associatedtype Prototype
    init(copy: Prototype)
    init(deepCopy: Prototype)
}
class C : Copyable {
    typealias Prototype = C // <-- requires adding this line to classes
    required init(copy: Prototype) {
        // Perform your copying here.
    }
    required init(deepCopy: Prototype) {
        // Perform your deep copying here.
    }
    @objc func copyWithZone(zone: NSZone) -> AnyObject {
        return Prototype(copy: self)
    }
}

1

У меня была аналогичная проблема, и я придумал кое-что, что может быть полезно, поэтому я хотел бы поделиться ею для справок в будущем, потому что это одно из первых мест, которые я нашел при поиске решения.

Как указано выше, проблема заключается в неоднозначности типа возвращаемого значения для функции copy (). Это можно очень четко проиллюстрировать, разделив функции copy () -> C и copy () -> P:

Итак, предполагая, что вы определяете протокол и класс следующим образом:

protocol P
{
   func copy() -> P
}

class C:P  
{        
   func doCopy() -> C { return C() }       
   func copy() -> C   { return doCopy() }
   func copy() -> P   { return doCopy() }       
}

Это компилирует и дает ожидаемые результаты, если тип возвращаемого значения явный. Каждый раз, когда компилятор должен решить, каким должен быть возвращаемый тип (сам по себе), он обнаружит, что ситуация неоднозначна и завершится ошибкой для всех конкретных классов, реализующих протокол P.

Например:

var aC:C = C()   // aC is of type C
var aP:P = aC    // aP is of type P (contains an instance of C)

var bC:C         // this to test assignment to a C type variable
var bP:P         //     "       "         "      P     "    "

bC = aC.copy()         // OK copy()->C is used

bP = aC.copy()         // Ambiguous. 
                       // compiler could use either functions
bP = (aC as P).copy()  // but this resolves the ambiguity.

bC = aP.copy()         // Fails, obvious type incompatibility
bP = aP.copy()         // OK copy()->P is used

В заключение, это будет работать в ситуациях, когда вы либо не используете функцию copy () базового класса, либо у вас всегда есть явный контекст типа.

Я обнаружил, что использую то же имя функции, что и конкретный класс, созданный для громоздкого кода повсюду, поэтому в итоге я использовал другое имя для функции copy () протокола.

Конечный результат больше похож:

protocol P
{
   func copyAsP() -> P
}

class C:P  
{
   func copy() -> C 
   { 
      // there usually is a lot more code around here... 
      return C() 
   }
   func copyAsP() -> P { return copy() }       
}

Конечно, мой контекст и функции совершенно разные, но, исходя из сути вопроса, я постарался максимально приблизиться к приведенному примеру.


1

Swift 5.1 теперь позволяет принудительное применение к себе, as! Self

  1> protocol P { 
  2.     func id() -> Self 
  3. } 
  9> class D : P { 
 10.     func id() -> Self { 
 11.         return D()
 12.     } 
 13. } 
error: repl.swift:11:16: error: cannot convert return expression of type 'D' to return type 'Self'
        return D()
               ^~~
                   as! Self


  9> class D : P { 
 10.     func id() -> Self { 
 11.         return D() as! Self
 12.     } 
 13. } //works

0

Просто бросаю здесь свою шляпу на ринг. Нам был нужен протокол, который возвращал бы необязательный тип, к которому был применен протокол. Мы также хотели, чтобы переопределение явно возвращало тип, а не только Self.

Уловка заключается в том, что вместо того, чтобы использовать «Self» в качестве возвращаемого типа, вы вместо этого определяете связанный тип, который вы устанавливаете равным Self, а затем используете этот связанный тип.

Вот старый способ, используя Self ...

protocol Mappable{
    static func map() -> Self?
}

// Generated from Fix-it
extension SomeSpecificClass : Mappable{
    static func map() -> Self? {
        ...
    }
}

Вот новый способ использования связанного типа. Обратите внимание, что тип возвращаемого значения теперь явный, а не «Self».

protocol Mappable{
    associatedtype ExplicitSelf = Self
    static func map() -> ExplicitSelf?
}

// Generated from Fix-it
extension SomeSpecificClass : Mappable{
    static func map() -> SomeSpecificClass? {
        ...
    }
}

0

Чтобы добавить к ответам associatedtypeспособ, я предлагаю переместить создание экземпляра в стандартную реализацию расширения протокола. Таким образом, соответствующим классам не придется его реализовывать, что избавит нас от дублирования кода:

protocol Initializable {
    init()
}

protocol Creatable: Initializable {
    associatedtype Object: Initializable = Self
    static func newInstance() -> Object
}

extension Creatable {
    static func newInstance() -> Object {
        return Object()
    }
}

class MyClass: Creatable {
    required init() {}
}

class MyOtherClass: Creatable {
    required init() {}
}

// Any class (struct, etc.) conforming to Creatable
// can create new instances without having to implement newInstance() 
let instance1 = MyClass.newInstance()
let instance2 = MyOtherClass.newInstance()
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.