Функція протоколу, що повертається 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, або Self як повернене значення, якщо тільки classafinal class
aububans

6
Добре, я відтворив помилки, але під час запитань потрібно вказати фактичну помилку, яка повертається. Не просто "це дає помилки" або "це не працює".
Роб Нейпір,

Тут компілятор абсолютно правильний у своїх помилках, до речі. Я просто думаю про те, чи можете ви взагалі отримати те, що намагаєтесь зробити.
Роб Нейпір,

1
Але можна зателефонувати [[[self class] alloc] init]. Отже, я думаю, питання в тому, чи існує безпечний для виклику спосіб виклику поточного класу та виклику методу init?
aeubanks

Відповіді:


144

Проблема полягає в тому, що ви даєте обіцянку, яку компілятор не може довести, що ви її дотримаєте.

Отже, ви створили цю обіцянку: дзвінок copy()поверне власний тип, повністю ініціалізований.

Але потім ви реалізували copy()такий спосіб:

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

Тепер я підклас, який не замінює copy(). І я повертаю a C, не повністю ініціалізований Self(що я обіцяв). Тож це нічого доброго. Як щодо:

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

Ну, це не буде компілювати, але навіть якби це зробило, це не було б корисно. Підклас може не мати тривіального конструктора, тому D()може бути навіть не законним. (Хоча дивіться нижче.)

Добре, а як щодо:

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

Так, але це не повертається Self. Воно повертається C. Ви все ще не виконуєте обіцянку.

"Але ObjC може це зробити!" Ну, начебто. Здебільшого тому, що все одно, чи дотримаєш ти обіцянку так, як Свіфт. Якщо вам не вдається реалізувати copyWithZone:в підкласі, можливо, ви не зможете повністю ініціалізувати свій об'єкт. Компілятор навіть не попередить вас, що ви це зробили.

"Але більшість всього в ObjC можна перекласти на Swift, і ObjC це робить NSCopying". Так, і ось як це визначається:

func copy() -> AnyObject!

Отже, ви можете зробити те саме (тут немає причин!):

protocol Copyable {
  func copy() -> AnyObject
}

Це говорить: "Я нічого не обіцяю щодо того, що ви повернете". Ви також можете сказати:

protocol Copyable {
  func copy() -> Copyable
}

Це обіцянка, яку ти можеш дати.

Але ми можемо трохи подумати про С ++ і пам’ятати, що ми можемо обіцяти . Ми можемо пообіцяти, що ми та всі наші підкласи будемо реалізовувати певні типи ініціалізаторів, і 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

2
Останнє рішення pure-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

Існує ще один спосіб зробити те, що ви хочете, включаючи скористатися перевагами асоційованого типу Свіфта. Ось простий приклад:

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

тобто бовтання розуму. Ого. Це те, про що я тут прошу, насправді є варіацією цього ?? stackoverflow.com/q/42041150/294884
Fattie

@JoeBlow Я боюся, що це не так. Я б сказав, що для збереження розуму ми повинні точно знати тип повернення (тобто не "А або В", а просто "А"; інакше ми повинні думати про поліморфізм + успадкування + перевантаження функції (принаймні).
werediver

це обман компілятора. Оскільки перевизначення foo()не застосовується, кожен Vehicleнащадок без foo()спеціальної реалізації спричинить очевидний збій autocast(). Наприклад: class SuperCar: Vehicle { } let superCar = SuperCar.foo() . Екземпляр Vehicleнеможливо скоротити, SuperCarтому примусове розгортання нуля в '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.