Giao thức func tự trả lại


82

Tôi có một giao thức P trả về một bản sao của đối tượng:

protocol P {
    func copy() -> Self
}

và một lớp C thực hiện P:

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

Tuy nhiên, liệu tôi có đặt giá trị trả về hay không khi Selfgặp lỗi sau:

Không thể chuyển đổi biểu thức trả về kiểu 'C' thành kiểu trả về 'Bản thân'

Tôi cũng đã thử quay lại C.

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

Điều đó dẫn đến lỗi sau:

Phương thức 'copy ()' trong lớp không phải cuối cùng 'C' phải trở lại Selfđể tuân theo giao thức 'P'

Không có gì hoạt động ngoại trừ trường hợp tôi đặt tiền tố class Cvới finalie do:

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

Tuy nhiên nếu tôi muốn phân lớp C thì không có gì hoạt động. Có cách nào để khắc phục điều này?


1
Ý bạn là gì khi "không có gì hoạt động?"
Rob Napier

Trình biên dịch phàn nàn khi đưa hoặc C hoặc tự như giá trị trả trừ khi classlà mộtfinal class
aeubanks

6
OK, tôi đã tạo lại các lỗi, nhưng khi đặt câu hỏi, bạn cần bao gồm lỗi thực tế được trả về. Không chỉ "nó đưa ra lỗi" hoặc "nó không hoạt động."
Rob Napier

Trình biên dịch hoàn toàn chính xác trong các lỗi của nó ở đây, BTW. Tôi chỉ đang nghĩ xem liệu bạn có thể làm được điều mà bạn đang cố gắng làm hay không.
Rob Napier

1
Nhưng bạn có thể gọi [[[self class] alloc] init]. Vì vậy, tôi đoán câu hỏi đặt ra là có cách nào an toàn về kiểu để gọi lớp hiện tại và gọi một phương thức init không?
aeubanks

Câu trả lời:


144

Vấn đề là bạn đang thực hiện một lời hứa mà trình biên dịch không thể chứng minh rằng bạn sẽ giữ.

Vì vậy, bạn đã tạo lời hứa này: Gọi copy()sẽ trả về kiểu riêng của nó, được khởi tạo hoàn toàn.

Nhưng sau đó bạn thực hiện copy()theo cách này:

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

Bây giờ tôi là một lớp con không ghi đè copy(). Và tôi trả về một C, không phải là một khởi tạo hoàn toàn Self(mà tôi đã hứa). Vì vậy, đó là không tốt. Làm thế nào về:

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

Chà, điều đó sẽ không biên dịch, nhưng ngay cả khi nó có, nó sẽ không tốt. Lớp con có thể không có hàm tạo tầm thường, vì vậy D()thậm chí có thể không hợp pháp. (Mặc dù xem bên dưới.)

OK, vậy thì sao về:

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

Có, nhưng điều đó không quay trở lại Self. Nó trở lại C. Bạn vẫn không giữ lời hứa của mình.

"Nhưng objC có thể làm được!" Chà, đại loại. Chủ yếu là vì nó không quan tâm nếu bạn giữ lời hứa của mình như Swift làm. Nếu bạn không thực hiện được copyWithZone:trong lớp con, bạn có thể không khởi tạo hoàn toàn đối tượng của mình. Trình biên dịch thậm chí sẽ không cảnh báo bạn rằng bạn đã làm điều đó.

"Nhưng hầu hết mọi thứ trong ObjC đều có thể được dịch sang Swift, và ObjC thì có NSCopying." Có, nó có, và đây là cách nó được định nghĩa:

func copy() -> AnyObject!

Vì vậy, bạn có thể làm tương tự (không có lý do gì cho! Ở đây):

protocol Copyable {
  func copy() -> AnyObject
}

Điều đó nói rằng "Tôi không hứa hẹn bất cứ điều gì về những gì bạn nhận lại." Bạn cũng có thể nói:

protocol Copyable {
  func copy() -> Copyable
}

Đó là một lời hứa bạn có thể thực hiện.

Nhưng chúng ta có thể nghĩ về C ++ một chút và nhớ rằng có một lời hứa mà chúng ta có thể thực hiện. Chúng tôi có thể hứa rằng chúng tôi và tất cả các lớp con của chúng tôi sẽ triển khai các loại trình khởi tạo cụ thể và Swift sẽ thực thi điều đó (và do đó có thể chứng minh rằng chúng tôi đang nói sự thật):

protocol Copyable {
  init(copy: Self)
}

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

Và đó là cách bạn nên thực hiện các bản sao.

Chúng ta có thể tiến thêm một bước nữa, nhưng nó vẫn sử dụng dynamicTypevà tôi chưa thử nghiệm rộng rãi để đảm bảo rằng đó luôn là những gì chúng ta muốn, nhưng nó phải chính xác:

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.
  }
}

Ở đây chúng tôi hứa rằng có một bộ khởi tạo thực hiện các bản sao cho chúng tôi và sau đó chúng tôi có thể xác định cái nào sẽ gọi trong thời gian chạy, cung cấp cho chúng tôi cú pháp phương thức mà bạn đang tìm kiếm.


Hmm, họ phải đã thay đổi điều này. Tôi có thể đã thề rằng nó func copy() -> Choạt động trong các bản beta trước đó và nó nhất quán vì tính tuân thủ giao thức không được kế thừa. (Bây giờ có vẻ như giao thức phù hợp được thừa kế, và func copy() -> Ckhông làm việc.)
newacct

2
Giải pháp Swift tinh khiết cuối cùng không hoạt động với các lớp con vì chúng bắt buộc phải triển khai init(copy: C)thay vào đó init(copy: Self):(
liquidsonic

Giải pháp cuối cùng đảm bảo giá trị trả về là Selfnhưng trình khởi tạo sau đó tất cả phải chấp nhận một biến được nhập tĩnh C, có nghĩa là, nó không cải thiện nhiều nếu chỉ trả về AnyObjectở vị trí đầu tiên.
chakrit

1
Trong nhanh chóng 2.0, bạn sẽ phải gọi init một cách rõ ràng:self.dynamicType.init( ... )
pronebird

1
@Dschee bên trong C, Bản thân có thể là C hoặc một lớp con của C. Đó là những kiểu khác nhau.
Rob Napier

25

Với Swift 2, chúng ta có thể sử dụng các phần mở rộng giao thức cho việc này.

protocol Copyable {
    init(copy:Self)
}

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

Đây là một câu trả lời tuyệt vời và kiểu của phương pháp đã được thảo luận rộng rãi trên WWDC 2015.
gkaimakas

2
Đây phải là câu trả lời được chấp nhận. Nó có thể được đơn giản hóa với return Self(copy: self)(ít nhất là trong Swift 2.2).
jhrmnn

16

Có một cách khác để làm những gì bạn muốn liên quan đến việc tận dụng kiểu liên kết của Swift. Đây là một ví dụ đơn giản:

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()

Hấp dẫn. Tôi tự hỏi liệu điều đó có liên quan đến stackoverflow.com/q/42041150/294884
Fattie

Cái này làm những gì tôi quan tâm. Cảm ơn!
Josh tại The Nerdery

10

Trên thực tế, có một thủ thuật cho phép dễ dàng quay lại Selfkhi được giao thức yêu cầu ( ý chính ):

/// 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
Chà. biên dịch. Đó là tinh quái, bởi vì trình biên dịch sẽ không cho phép bạn chỉreturn Vehicle() as! Self
SimplGy

đó là suy nghĩ lung tung. Chà. Điều tôi đang hỏi ở đây có thực sự là một biến thể về điều này không ?? stackoverflow.com/q/42041150/294884
Fattie

@JoeBlow Tôi e rằng không phải vậy. Tôi muốn nói rằng để giữ cho tâm trí của chúng ta an toàn, chúng ta nên biết chính xác kiểu trả về (tức là không phải "A hay B", mà chỉ là "A"; nếu không, chúng ta phải nghĩ về đa hình + kế thừa + nạp chồng hàm (ít nhất)).
werediver

đó là lừa trình biên dịch. Vì ghi đè foo()không được thực thi, mọi Vehiclecon cháu không có foo()triển khai tùy chỉnh sẽ tạo ra sự cố rõ ràng autocast(). Ví dụ: class SuperCar: Vehicle { } let superCar = SuperCar.foo() . Trường hợp của Vehiclekhông thể bị từ chối SuperCar- vì vậy buộc bỏ gói nil trong 'autocast ()' dẫn đến sự cố.
freennnn

1
@freennnn Việc thay đổi mã thành mã sau sẽ không gặp sự cố khi lớp con không ghi đè foo(). Yêu cầu duy nhất là lớp Foophải có bộ khởi tạo bắt buộc để nó hoạt động như hình dưới đây. 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

Theo gợi ý của Rob, điều này có thể được thực hiện chung chung hơn với các loại liên quan . Tôi đã thay đổi ví dụ một chút để chứng minh lợi ích của phương pháp này.

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

Tôi đã gặp sự cố tương tự và đã nghĩ ra một thứ có thể hữu ích nên tôi sẽ chia sẻ nó để tham khảo trong tương lai vì đây là một trong những nơi đầu tiên tôi tìm thấy khi tìm kiếm giải pháp.

Như đã nêu ở trên, vấn đề là sự không rõ ràng của kiểu trả về cho hàm copy (). Điều này có thể được minh họa rất rõ ràng bằng cách tách các hàm copy () -> C và copy () -> P:

Vì vậy, giả sử bạn xác định giao thức và lớp như sau:

protocol P
{
   func copy() -> P
}

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

Điều này biên dịch và tạo ra kết quả mong đợi khi kiểu của giá trị trả về là rõ ràng. Bất kỳ lúc nào trình biên dịch phải quyết định kiểu trả về (tự nó), nó sẽ thấy tình huống không rõ ràng và không thành công đối với tất cả các lớp cụ thể thực hiện giao thức P.

Ví dụ:

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

Tóm lại, điều này sẽ hoạt động trong các tình huống mà bạn không sử dụng hàm copy () của lớp cơ sở hoặc bạn luôn có ngữ cảnh kiểu rõ ràng.

Tôi nhận thấy rằng việc sử dụng cùng một tên hàm với lớp cụ thể được tạo cho mã khó sử dụng ở khắp mọi nơi, vì vậy tôi đã sử dụng một tên khác cho hàm copy () của giao thức.

Kết quả cuối cùng giống như sau:

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() }       
}

Tất nhiên bối cảnh và chức năng của tôi hoàn toàn khác nhau nhưng trên tinh thần của câu hỏi, tôi cố gắng bám sát ví dụ được đưa ra nhất có thể.


1

Swift 5.1 giờ đây cho phép ép buộc thành Tự, 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

Chỉ cần ném mũ của tôi vào vòng ở đây. Chúng tôi cần một giao thức trả về một tùy chọn của loại giao thức được áp dụng. Chúng tôi cũng muốn ghi đè để trả về loại một cách rõ ràng, không chỉ là Tự.

Mẹo là thay vì sử dụng 'Bản thân' làm kiểu trả về, thay vào đó bạn xác định kiểu được liên kết mà bạn đặt bằng Bản thân, sau đó sử dụng kiểu được liên kết đó.

Đây là cách cũ, sử dụng Tự ...

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

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

Đây là cách mới sử dụng kiểu được liên kết. Lưu ý rằng kiểu trả về bây giờ là rõ ràng, không phải là 'Tự'.

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

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

0

Để thêm vào câu trả lời với associatedtypecách này, tôi khuyên bạn nên chuyển việc tạo phiên bản sang triển khai mặc định của phần mở rộng giao thức. Theo cách đó, các lớp tuân thủ sẽ không phải triển khai nó, do đó giúp chúng ta tránh được sự trùng lặp mã:

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()
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.