วิธีใช้โปรโตคอลทั่วไปเป็นประเภทตัวแปร


89

สมมติว่าฉันมีโปรโตคอล:

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

และนี่คือการนำไปใช้งาน

class Printer<T> : Printable {

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

ความคาดหวังของฉันคือฉันต้องสามารถใช้Printableตัวแปรเพื่อพิมพ์ค่าเช่นนี้:

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

คอมไพเลอร์บ่นด้วยข้อผิดพลาดนี้:

"โปรโตคอล" พิมพ์ได้ "สามารถใช้เป็นข้อ จำกัด ทั่วไปเท่านั้นเนื่องจากมีข้อกำหนดในตนเองหรือประเภทที่เกี่ยวข้อง

ฉันทำอะไรผิดหรือเปล่า? เพื่อแก้ไขปัญหานี้หรือไม่?

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

แก้ไข 2: ตัวอย่างโลกแห่งความจริงของสิ่งที่ฉันต้องการ โปรดทราบว่าสิ่งนี้จะไม่รวบรวม แต่นำเสนอสิ่งที่ฉันต้องการบรรลุ

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
นี่คือชุดข้อความที่เกี่ยวข้องในฟอรัมนักพัฒนาของ Apple ตั้งแต่ปี 2014 ซึ่งคำถามนี้ได้รับการตอบสนอง (ในระดับหนึ่ง) โดยนักพัฒนา Swift ที่ Apple: devforums.apple.com/thread/230611 (หมายเหตุ: ต้องใช้บัญชีนักพัฒนา Apple เพื่อดูสิ่งนี้ page.)
titaniumdecoy

คำตอบ:


88

ดังที่ Thomas ชี้ให้เห็นคุณสามารถประกาศตัวแปรของคุณได้โดยไม่ต้องระบุประเภทเลย (หรืออาจระบุเป็นประเภทอย่างชัดเจนPrinter<Int>ก็ได้ แต่นี่คือคำอธิบายว่าเหตุใดคุณจึงไม่มีประเภทของPrintableโปรโตคอล

คุณไม่สามารถปฏิบัติต่อโปรโตคอลที่มีประเภทที่เกี่ยวข้องเช่นโปรโตคอลทั่วไปและประกาศเป็นประเภทตัวแปรแบบสแตนด์อโลน หากต้องการคิดว่าเหตุใดให้พิจารณาสถานการณ์นี้ สมมติว่าคุณประกาศโปรโตคอลสำหรับจัดเก็บบางประเภทโดยพลการแล้วดึงกลับมา:

// 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"

โอเคจนถึงตอนนี้ดีมาก

ตอนนี้เหตุผลหลักที่คุณจะต้องมีประเภทของตัวแปรเป็นโปรโตคอลที่ใช้ประเภทแทนที่จะเป็นประเภทจริงคือเพื่อให้คุณสามารถกำหนดชนิดของวัตถุต่างๆที่ทั้งหมดสอดคล้องกับโปรโตคอลนั้นให้กับตัวแปรเดียวกันและรับความหลากหลาย พฤติกรรมที่รันไทม์ขึ้นอยู่กับสิ่งที่เป็นจริง

แต่คุณไม่สามารถทำได้หากโปรโตคอลมีประเภทที่เกี่ยวข้อง โค้ดต่อไปนี้จะทำงานอย่างไรในทางปฏิบัติ

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

ในโค้ดด้านบนประเภทxจะเป็นอย่างไร? อันInt? หรือกString? ใน Swift ทุกประเภทต้องได้รับการแก้ไขในเวลาคอมไพล์ ฟังก์ชันไม่สามารถเปลี่ยนแบบไดนามิกจากการส่งคืนประเภทหนึ่งไปยังอีกประเภทหนึ่งตามปัจจัยที่กำหนดในรันไทม์

แต่คุณสามารถใช้StoredTypeเป็นข้อ จำกัด ทั่วไปเท่านั้น สมมติว่าคุณต้องการพิมพ์ประเภทจัดเก็บประเภทใดก็ได้ คุณสามารถเขียนฟังก์ชันดังนี้:

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

printStoredValue(intStorer)
printStoredValue(stringStorer)

ไม่เป็นไรเพราะในเวลาคอมไพล์เหมือนกับว่าคอมไพเลอร์เขียนสองเวอร์ชันprintStoredValue: หนึ่งสำหรับInts และหนึ่งสำหรับStrings ภายในสองเวอร์ชันxนี้เป็นที่ทราบกันดีว่าเป็นประเภทเฉพาะ


20
กล่าวอีกนัยหนึ่งไม่มีวิธีใดที่จะมีโปรโตคอลทั่วไปเป็นพารามิเตอร์และเหตุผลก็คือ Swift ไม่รองรับการสนับสนุนรันไทม์สไตล์. NET ของ generics? นี้ค่อนข้างไม่สะดวก
Tamerlane

ความรู้. NET ของฉันค่อนข้างมืดมน ... คุณมีตัวอย่างของสิ่งที่คล้ายกันใน. NET ที่ใช้งานได้ในตัวอย่างนี้หรือไม่? นอกจากนี้มันยากเล็กน้อยที่จะดูว่าโปรโตคอลในตัวอย่างของคุณซื้ออะไรให้คุณ ที่รันไทม์สิ่งที่คุณจะคาดหวังว่าพฤติกรรมที่จะถ้าคุณได้รับมอบหมายเครื่องพิมพ์ประเภทที่แตกต่างกันที่คุณpตัวแปรแล้วผ่านเข้าไปในประเภทที่ไม่ถูกต้องprint? ข้อยกเว้นรันไทม์?
Airspeed Velocity

@AirspeedVelocity ฉันได้อัปเดตคำถามเพื่อรวมตัวอย่าง C # สำหรับคุณค่าที่ฉันต้องการก็คือสิ่งนี้จะช่วยให้ฉันพัฒนาไปยังอินเทอร์เฟซที่ไม่ใช่การใช้งาน หากฉันต้องการส่งผ่านการพิมพ์ไปยังฟังก์ชันฉันสามารถใช้อินเทอร์เฟซในการประกาศและส่งผ่านการใช้งานที่แตกต่างมากมายโดยไม่ต้องสัมผัสฟังก์ชันของฉัน ลองนึกถึงการใช้ไลบรารีคอลเลกชันคุณจะต้องใช้รหัสประเภทนี้รวมทั้งข้อ จำกัด เพิ่มเติมในประเภท T.
Tamerlane

4
ในทางทฤษฎีหากสามารถสร้างโปรโตคอลทั่วไปโดยใช้วงเล็บเหลี่ยมเช่นใน C # ได้จะอนุญาตให้สร้างตัวแปรประเภทโปรโตคอลหรือไม่ (StoringType <Int>, StoringType <String>)
GeRyCh

1
ใน Java คุณสามารถทำได้เทียบเท่าvar someStorer: StoringType<Int>หรือvar someStorer: StoringType<String>และแก้ปัญหาที่คุณร่างไว้
JeremyP

43

มีวิธีการแก้ปัญหาหนึ่งที่ยังไม่ได้รับการกล่าวถึงในคำถามนี้ซึ่งใช้เทคนิคที่เรียกว่าเป็นประเภทการลบออก เพื่อให้ได้อินเทอร์เฟซนามธรรมสำหรับโปรโตคอลทั่วไปให้สร้างคลาสหรือโครงสร้างที่ห่อหุ้มอ็อบเจ็กต์หรือโครงสร้างที่สอดคล้องกับโปรโตคอล คลาส wrapper ซึ่งมักมีชื่อว่า 'Any {protocol name}' นั้นเป็นไปตามโปรโตคอลและใช้ฟังก์ชันของมันโดยส่งต่อการเรียกทั้งหมดไปยังวัตถุภายใน ลองดูตัวอย่างด้านล่างในสนามเด็กเล่น:

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

ประเภทของprinterเป็นที่รู้จักAnyPrinter<Int>และสามารถนำมาใช้เพื่อสรุปการใช้งานโปรโตคอลเครื่องพิมพ์ที่เป็นไปได้ แม้ว่า AnyPrinter จะไม่ได้เป็นนามธรรมในทางเทคนิค แต่การนำไปใช้งานนั้นเป็นเพียงการลดลงของประเภทการใช้งานจริงและสามารถใช้เพื่อแยกประเภทการใช้งานออกจากประเภทที่ใช้งานได้

สิ่งหนึ่งที่ควรทราบคือAnyPrinterไม่จำเป็นต้องเก็บรักษาอินสแตนซ์พื้นฐานไว้อย่างชัดเจน ในความเป็นจริงเราไม่สามารถเพราะเราไม่สามารถประกาศว่าAnyPrinterมีPrinter<T>ทรัพย์สิน แต่เราได้รับฟังก์ชั่นตัวชี้_printไปที่ฐานของprintฟังก์ชั่น การเรียกbase.printโดยไม่เรียกใช้จะส่งคืนฟังก์ชันที่ฐานถูก curried เป็นตัวแปร self และจะถูกเก็บไว้สำหรับการเรียกใช้ในอนาคต

สิ่งที่ควรทราบอีกประการหนึ่งก็คือโซลูชันนี้เป็นอีกชั้นหนึ่งของการจัดส่งแบบไดนามิกซึ่งหมายถึงประสิทธิภาพเล็กน้อย นอกจากนี้อินสแตนซ์การลบประเภทต้องใช้หน่วยความจำเพิ่มเติมที่ด้านบนของอินสแตนซ์ที่อยู่เบื้องหลัง ด้วยเหตุผลเหล่านี้การลบประเภทจึงไม่ใช่นามธรรมที่ไม่มีค่าใช้จ่าย

เห็นได้ชัดว่ามีงานบางอย่างในการตั้งค่าการลบประเภท แต่จะมีประโยชน์มากหากจำเป็นต้องใช้นามธรรมโปรโตคอลทั่วไป AnySequenceรูปแบบนี้จะพบในห้องสมุดมาตรฐานรวดเร็วกับชนิดเช่น อ่านเพิ่มเติม: http://robnapier.net/erasure

โบนัส:

หากคุณตัดสินใจว่าต้องการใช้การใช้งานแบบเดียวกันกับPrinterทุกที่คุณสามารถจัดเตรียมเครื่องมือเริ่มต้นที่สะดวกสำหรับการAnyPrinterฉีดประเภทนั้น

extension AnyPrinter {

    convenience init() {

        let nsLogger = NSLogger<T>()

        self.init(base: nsLogger)
    }
}

let printer = AnyPrinter<Int>()

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

นี่อาจเป็นวิธีที่ง่ายและแห้งเร็วในการแสดงการฉีดพึ่งพาสำหรับโปรโตคอลที่คุณใช้ในแอปของคุณ


ขอบคุณสำหรับสิ่งนี้. ฉันชอบรูปแบบของการลบประเภทนี้ (โดยใช้ตัวชี้ฟังก์ชัน) ดีกว่าการใช้คลาสนามธรรม (ซึ่งแน่นอนว่าไม่มีอยู่จริงและต้องแกล้งทำโดยใช้fatalError()) ที่อธิบายไว้ในแบบฝึกหัดการลบประเภทอื่น ๆ
Chase

4

ระบุกรณีการใช้งานที่อัปเดตของคุณ:

(btw Printableเป็นโปรโตคอล Swift มาตรฐานอยู่แล้วดังนั้นคุณอาจต้องการเลือกชื่ออื่นเพื่อหลีกเลี่ยงความสับสน)

ในการบังคับใช้ข้อ จำกัด เฉพาะกับผู้ใช้โปรโตคอลคุณสามารถ จำกัด ประเภทของโปรโตคอลได้ ดังนั้นในการสร้างคอลเลกชันโปรโตคอลของคุณที่ต้องการให้พิมพ์องค์ประกอบได้:

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

ตอนนี้หากคุณต้องการใช้คอลเลคชันที่มีได้เฉพาะองค์ประกอบที่พิมพ์ได้:

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

อย่างไรก็ตามนี่อาจเป็นยูทิลิตี้จริงเพียงเล็กน้อยเนื่องจากคุณไม่สามารถ จำกัด โครงสร้างคอลเลคชัน Swift ที่มีอยู่เช่นนั้นได้เฉพาะที่คุณใช้เท่านั้น

คุณควรสร้างฟังก์ชันทั่วไปที่ จำกัด การป้อนข้อมูลให้กับคอลเล็กชันที่มีองค์ประกอบที่พิมพ์ได้

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

โอ้คนนี้ดูป่วย สิ่งที่ฉันต้องการคือมีโปรโตคอลที่รองรับทั่วไป ฉันหวังว่าจะมีอะไรแบบนี้: protocol Collection <T>: SequenceType และนั่นแหล่ะ ขอบคุณสำหรับตัวอย่างโค้ดฉันคิดว่าจะใช้เวลาสักพักในการย่อยมัน :)
Tamerlane
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.