Функция протокола возвращает 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.