Cách sử dụng giao thức chung chung như một loại biến


89

Giả sử tôi có một giao thức:

public protocol Printable {
    typealias T
    func Print(val:T)
}

Và đây là cách thực hiện

class Printer<T> : Printable {

    func Print(val: T) {
        println(val)
    }
}

Kỳ vọng của tôi là tôi phải có thể sử dụng Printablebiến để in các giá trị như sau:

let p:Printable = Printer<Int>()
p.Print(67)

Trình biên dịch đang phàn nàn với lỗi này:

"Giao thức 'Có thể in' chỉ có thể được sử dụng như một ràng buộc chung vì nó có các yêu cầu về loại riêng hoặc được liên kết"

Tôi có làm điều gì sai ? Dù sao để sửa chữa điều này?

**EDIT :** Adding similar code that works in C#

public interface IPrintable<T> 
{
    void Print(T val);
}

public class Printer<T> : IPrintable<T>
{
   public void Print(T val)
   {
      Console.WriteLine(val);
   }
}


//.... inside Main
.....
IPrintable<int> p = new Printer<int>();
p.Print(67)

CHỈNH SỬA 2: Ví dụ thực tế về những gì tôi muốn. Lưu ý rằng điều này sẽ không biên dịch, nhưng trình bày những gì tôi muốn đạt được.

protocol Printable 
{
   func Print()
}

protocol CollectionType<T where T:Printable> : SequenceType 
{
   .....
   /// here goes implementation
   ..... 
}

public class Collection<T where T:Printable> : CollectionType<T>
{
    ......
}

let col:CollectionType<Int> = SomeFunctiionThatReturnsIntCollection()
for item in col {
   item.Print()
}

1
Đây là một chuỗi liên quan trên Diễn đàn nhà phát triển Apple từ năm 2014, nơi câu hỏi này được giải quyết (ở một mức độ) bởi một nhà phát triển Swift tại Apple: devforums.apple.com/thread/230611 (Lưu ý: Cần có Tài khoản nhà phát triển Apple để xem thông tin này trang).
titaniumdecoy

Câu trả lời:


88

Như Thomas đã chỉ ra, bạn có thể khai báo biến của mình bằng cách không đưa ra một kiểu nào (hoặc bạn có thể cho nó là kiểu một cách rõ ràng Printer<Int>. Nhưng đây là giải thích tại sao bạn không thể có một kiểu Printablegiao thức.

Bạn không thể coi các giao thức có các kiểu liên kết như các giao thức thông thường và khai báo chúng là các kiểu biến độc lập. Để suy nghĩ về lý do tại sao, hãy xem xét kịch bản này. Giả sử bạn đã khai báo một giao thức để lưu trữ một số kiểu tùy ý và sau đó tìm nạp nó trở lại:

// a general protocol that allows for storing and retrieving
// a specific type (as defined by a Stored typealias
protocol StoringType {
    typealias Stored

    init(_ value: Stored)
    func getStored() -> Stored
}

// An implementation that stores Ints
struct IntStorer: StoringType {
    typealias Stored = Int
    private let _stored: Int
    init(_ value: Int) { _stored = value }
    func getStored() -> Int { return _stored }
}

// An implementation that stores Strings
struct StringStorer: StoringType {
    typealias Stored = String
    private let _stored: String
    init(_ value: String) { _stored = value }
    func getStored() -> String { return _stored }
}

let intStorer = IntStorer(5)
intStorer.getStored() // returns 5

let stringStorer = StringStorer("five")
stringStorer.getStored() // returns "five"

OK, cho đến nay rất tốt.

Bây giờ, lý do chính mà bạn muốn có một loại biến là một giao thức mà một loại thực hiện, chứ không phải là loại thực tế, là để bạn có thể gán các loại đối tượng khác nhau mà tất cả đều tuân theo giao thức đó cho cùng một biến và nhận được đa hình hành vi trong thời gian chạy tùy thuộc vào đối tượng thực sự là gì.

Nhưng bạn không thể làm điều này nếu giao thức có kiểu được liên kết. Đoạn mã sau hoạt động như thế nào trong thực tế?

// as you've seen this won't compile because
// StoringType has an associated type.

// randomly assign either a string or int storer to someStorer:
var someStorer: StoringType = 
      arc4random()%2 == 0 ? intStorer : stringStorer

let x = someStorer.getStored()

Trong đoạn mã trên, loại xsẽ là gì? An Int? Hay a String? Trong Swift, tất cả các kiểu phải được cố định tại thời điểm biên dịch. Một hàm không thể tự động chuyển từ trả về kiểu này sang kiểu khác dựa trên các yếu tố được xác định trong thời gian chạy.

Thay vào đó, bạn chỉ có thể sử dụng StoredTypenhư một ràng buộc chung. Giả sử bạn muốn in ra bất kỳ loại kiểu lưu trữ nào. Bạn có thể viết một hàm như thế này:

func printStoredValue<S: StoringType>(storer: S) {
    let x = storer.getStored()
    println(x)
}

printStoredValue(intStorer)
printStoredValue(stringStorer)

Điều này không sao cả, bởi vì tại thời điểm biên dịch, nó giống như thể trình biên dịch viết ra hai phiên bản printStoredValue: một cho Ints và một cho Strings. Trong hai phiên bản đó, xđược biết là thuộc một loại cụ thể.


20
Nói cách khác, không có cách nào để có giao thức chung chung làm tham số và lý do là Swift không hỗ trợ hỗ trợ thời gian chạy kiểu .NET của generics? Điều này khá bất tiện.
Tamerlane,

Kiến thức .NET của tôi hơi mơ hồ ... bạn có ví dụ về thứ gì đó tương tự trong .NET sẽ hoạt động trong ví dụ này không? Ngoài ra, hơi khó để biết giao thức nào trong ví dụ của bạn đang mua bạn. Trong thời gian chạy, bạn sẽ mong đợi hành vi sẽ như thế nào nếu bạn gán các máy in thuộc các loại khác nhau cho pbiến của mình và sau đó chuyển các loại không hợp lệ vào print? Ngoại lệ thời gian chạy?
Vận tốc Airspeed

@AirspeedVelocity Tôi đã cập nhật câu hỏi để bao gồm ví dụ C #. Về giá trị tại sao tôi cần nó là điều này sẽ cho phép tôi phát triển lên một giao diện không phải để triển khai. Nếu tôi cần chuyển có thể in cho một hàm, tôi có thể sử dụng giao diện trong khai báo và chuyển nhiều triển khai khác nhau mà không cần chạm vào hàm của mình. Cũng suy nghĩ về việc thực hiện thư viện bộ sưu tập, bạn sẽ cần loại mã này cộng với những hạn chế bổ sung đối với loại T.
Tamerlane

4
Về mặt lý thuyết, nếu có thể tạo các giao thức chung bằng cách sử dụng dấu ngoặc nhọn như trong C #, thì việc tạo các biến của kiểu giao thức có được phép không? (StoringType <Int>, StoringType <String>)
GeRyCh

1
Trong Java, bạn có thể làm tương đương với var someStorer: StoringType<Int>hoặc var someStorer: StoringType<String>và giải quyết vấn đề bạn vạch ra.
JeremyP

42

Có một giải pháp nữa chưa được đề cập đến cho câu hỏi này, đó là sử dụng một kỹ thuật có tên là tẩy loại . Để đạt được giao diện trừu tượng cho một giao thức chung, hãy tạo một lớp hoặc cấu trúc bao bọc một đối tượng hoặc cấu trúc phù hợp với giao thức. Lớp wrapper, thường được đặt tên là 'Bất kỳ {tên giao thức}' nào, bản thân nó tuân theo giao thức và thực hiện các chức năng của nó bằng cách chuyển tiếp tất cả các lệnh gọi đến đối tượng bên trong. Hãy thử ví dụ dưới đây trong một sân chơi:

import Foundation

public protocol Printer {
    typealias T
    func print(val:T)
}

struct AnyPrinter<U>: Printer {

    typealias T = U

    private let _print: U -> ()

    init<Base: Printer where Base.T == U>(base : Base) {
        _print = base.print
    }

    func print(val: T) {
        _print(val)
    }
}

struct NSLogger<U>: Printer {

    typealias T = U

    func print(val: T) {
        NSLog("\(val)")
    }
}

let nsLogger = NSLogger<Int>()

let printer = AnyPrinter(base: nsLogger)

printer.print(5) // prints 5

Loại printerđược biết là AnyPrinter<Int>có thể được sử dụng để tóm tắt bất kỳ triển khai có thể có của giao thức Máy in. Mặc dù AnyPrinter không trừu tượng về mặt kỹ thuật, nhưng việc triển khai nó chỉ là một bước chuyển sang kiểu triển khai thực và có thể được sử dụng để tách các kiểu triển khai khỏi các kiểu sử dụng chúng.

Một điều cần lưu ý là AnyPrinter không nhất thiết phải giữ lại cá thể cơ sở một cách rõ ràng. Trên thực tế, chúng ta không thể vì chúng ta không thể tuyên bố AnyPrinterPrinter<T>tài sản. Thay vào đó, chúng ta nhận được một con trỏ hàm _printđến printhàm của cơ sở . Việc gọi base.printmà không cần gọi nó trả về một hàm trong đó cơ sở được gọi là biến self, và do đó được giữ lại cho các lần gọi trong tương lai.

Một điều khác cần lưu ý là giải pháp này về cơ bản là một lớp điều khiển động khác, có nghĩa là một chút ảnh hưởng đến hiệu suất. Ngoài ra, thể hiện xóa kiểu yêu cầu thêm bộ nhớ trên đối tượng cơ bản. Vì những lý do này, tẩy xóa kiểu không phải là một cách trừu tượng miễn phí.

Rõ ràng là có một số công việc để thiết lập tính năng xóa kiểu, nhưng nó có thể rất hữu ích nếu cần trừu tượng hóa giao thức chung. Mẫu này được tìm thấy trong thư viện tiêu chuẩn nhanh với các loại như AnySequence. Đọc thêm: http://robnapier.net/erasure

TẶNG KEM:

Nếu bạn quyết định muốn thực hiện cùng một cách triển khai Printerở mọi nơi, bạn có thể cung cấp một trình khởi tạo thuận tiện choAnyPrinter tiêm loại đó.

extension AnyPrinter {

    convenience init() {

        let nsLogger = NSLogger<T>()

        self.init(base: nsLogger)
    }
}

let printer = AnyPrinter<Int>()

printer.print(10) //prints 10 with NSLog

Đây có thể là một cách dễ dàng và KHÔ để thể hiện sự phụ thuộc vào các giao thức mà bạn sử dụng trên ứng dụng của mình.


Cám ơn vì cái này. Tôi thích kiểu xóa kiểu này (sử dụng con trỏ hàm) hơn là sử dụng một lớp trừu tượng (tất nhiên, không tồn tại và phải được làm giả bằng cách sử dụng fatalError()) được mô tả trong các hướng dẫn xóa kiểu khác.
Đuổi theo

4

Giải quyết trường hợp sử dụng đã cập nhật của bạn:

(btw Printableđã là một giao thức Swift tiêu chuẩn nên bạn có thể muốn chọn một tên khác để tránh nhầm lẫn)

Để thực thi các hạn chế cụ thể đối với người triển khai giao thức, bạn có thể hạn chế kiểu chữ của giao thức. Vì vậy, để tạo bộ sưu tập giao thức của bạn, yêu cầu các phần tử có thể in được:

// because of how how collections are structured in the Swift std lib,
// you’d first need to create a PrintableGeneratorType, which would be
// a constrained version of GeneratorType
protocol PrintableGeneratorType: GeneratorType {
    // require elements to be printable:
    typealias Element: Printable
}

// then have the collection require a printable generator
protocol PrintableCollectionType: CollectionType {
    typealias Generator: PrintableGenerator
}

Bây giờ nếu bạn muốn triển khai một tập hợp chỉ có thể chứa các phần tử có thể in được:

struct MyPrintableCollection<T: Printable>: PrintableCollectionType {
    typealias Generator = IndexingGenerator<T>
    // etc...
}

Tuy nhiên, điều này có lẽ ít tiện ích thực tế, vì bạn không thể ràng buộc các cấu trúc bộ sưu tập Swift hiện có như vậy, chỉ những cấu trúc bạn triển khai.

Thay vào đó, bạn nên tạo các hàm chung giới hạn đầu vào của chúng vào các bộ sưu tập chứa các phần tử có thể in được.

func printCollection
    <C: CollectionType where C.Generator.Element: Printable>
    (source: C) {
        for x in source {
            x.print()
        }
}

Ôi trời trông bệnh hoạn quá. Những gì tôi cần chỉ là có giao thức với sự hỗ trợ chung. Tôi đã hy vọng có một cái gì đó như thế này: Bộ sưu tập giao thức <T>: SequenceType. Và đó là nó. Cảm ơn các mẫu mã Tôi nghĩ rằng nó sẽ mất một thời gian để tiêu hóa nó :)
Tamerlane
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.