Değişken türü olarak genel protokol nasıl kullanılır


89

Diyelim ki bir protokolüm var:

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

Ve işte uygulama

class Printer<T> : Printable {

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

Beklentim, bunun Printablegibi değerleri yazdırmak için değişken kullanabilmem gerektiğiydi :

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

Derleyici şu hatadan şikayet ediyor:

"" Yazdırılabilir "protokolü yalnızca genel bir kısıtlama olarak kullanılabilir çünkü kendi kendine veya ilişkili tür gereksinimleri vardır"

Yanlış bir şey mi yapıyorum ? Neyse bunu düzeltmek için?

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

DÜZENLEME 2: Ne istediğime dair gerçek dünya örneği. Bunun derlenmeyeceğini, ancak ulaşmak istediğimi sunduğunu unutmayın.

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
Aşağıda, bu sorunun Apple'daki bir Swift geliştiricisi tarafından ele alındığı (bir dereceye kadar) Apple Geliştirici Forumlarında ilgili bir başlık yer almaktadır : devforums.apple.com/thread/230611 (Not: Bunu görüntülemek için bir Apple Geliştirici Hesabı gereklidir. sayfa.)
titaniumdecoy

Yanıtlar:


88

Thomas'ın belirttiği gibi, değişkeninizi hiç bir tür vermeyerek bildirebilirsiniz (veya bunu açıkça tür olarak da verebilirsiniz Printer<Int>. Ama işte neden bir Printableprotokol türüne sahip olamayacağınıza dair bir açıklama .

Normal protokoller gibi ilişkili türlere sahip protokolleri işleyemez ve bunları bağımsız değişken türleri olarak bildiremezsiniz. Nedenini düşünmek için bu senaryoyu düşünün. Bazı gelişigüzel türlerin depolanması ve ardından geri getirilmesi için bir protokol ilan ettiğinizi varsayalım:

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

Tamam, şimdiye kadar her şey yolunda.

Şimdi, bir değişken türüne sahip olmanızın ana nedeni, gerçek türden ziyade bir türün uyguladığı bir protokol olabilir, böylece hepsi bu protokole uyan farklı nesne türlerini aynı değişkene atayabilir ve polimorfik elde edebilirsiniz. nesnenin gerçekte ne olduğuna bağlı olarak çalışma zamanında davranış.

Ancak protokolün ilişkili bir türü varsa bunu yapamazsınız. Aşağıdaki kod pratikte nasıl çalışır?

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

Yukarıdaki kodda, türü ne olurdu x? An Int? Veya a String? Swift'de tüm türler derleme zamanında sabitlenmelidir. Bir işlev, çalışma zamanında belirlenen faktörlere bağlı olarak bir türü döndürmekten diğerine dinamik olarak geçemez.

Bunun yerine, yalnızca StoredTypegenel bir kısıtlama olarak kullanabilirsiniz . Her türlü depolanmış türü yazdırmak istediğinizi varsayalım. Bunun gibi bir fonksiyon yazabilirsiniz:

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

printStoredValue(intStorer)
printStoredValue(stringStorer)

Sorun değil, çünkü derleme zamanında derleyici iki sürüm yazıyor gibidir printStoredValue: biri Ints için diğeri Strings için. Bu iki versiyonda, xbelirli bir tip olduğu bilinmektedir.


20
Diğer bir deyişle, bir parametre olarak genel protokolü kullanmanın bir yolu yoktur ve bunun nedeni, Swift'in jeneriklerin .NET tarzı çalışma zamanı desteğini desteklememesidir. Bu oldukça sakıncalıdır.
Tamerlane

.NET bilgim biraz belirsiz ... Bu örnekte işe yarayacak .NET'te benzer bir örnek var mı? Ayrıca, örneğinizdeki protokolün sizi satın aldığını görmek biraz zor. Çalışma zamanında, pdeğişkeninize farklı türlerde yazıcılar atadıysanız ve sonra geçersiz türler aktardıysanız, davranışın ne olmasını beklersiniz print? Çalışma zamanı istisnası?
Hava Hızı Hızı

@AirspeedVelocity Soruyu C # örneğini içerecek şekilde güncelledim. Buna neden ihtiyacım olduğu konusunda değere gelince, bu benim uygulamaya değil bir arayüz geliştirmeme izin verecek. Bir işleve yazdırılabilir olarak geçmem gerekirse, bildirimde arabirimi kullanabilir ve işlevime dokunmadan birçok fark uygulamasına geçebilirim. Ayrıca koleksiyon kitaplığını uygulamayı düşünün, bu tür bir koda ve T türünde ek kısıtlamalara ihtiyacınız olacak.
Tamerlane

4
Teorik olarak, C # gibi açılı parantezleri kullanarak jenerik protokoller oluşturmak mümkün olsaydı, protokol tipi değişkenlerin oluşturulmasına izin verilir mi? (StoringType <Int>, StoringType <String>)
GeRyCh

1
Java'da, anlattığınız problemin eşdeğerini yapabilir var someStorer: StoringType<Int>veya var someStorer: StoringType<String>çözebilirsiniz.
JeremyP

43

Bu soruda bahsedilmeyen bir çözüm daha var, bu da tip silme denen bir teknik kullanıyor . Genel bir protokol için soyut bir arabirim elde etmek için, protokole uyan bir nesneyi veya yapıyı saran bir sınıf veya yapı oluşturun. Genellikle 'Herhangi bir {protokol adı}' olarak adlandırılan sarmalayıcı sınıfın kendisi protokole uyar ve tüm çağrıları dahili nesneye ileterek işlevlerini uygular. Aşağıdaki örneği oyun alanında deneyin:

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

Türü printerolarak bilinir AnyPrinter<Int>ve Yazıcı protokolünün olası herhangi bir uygulamasını soyutlamak için kullanılabilir. AnyPrinter teknik olarak soyut olmasa da, uygulaması gerçek bir uygulama türüne düşmedir ve uygulama türlerini bunları kullanan türlerden ayırmak için kullanılabilir.

Unutulmaması gereken bir nokta, AnyPrintertemel örneği açıkça saklamak zorunda olmamasıdır. Aslında, mülk sahibi olduğumuzu beyan edemediğimiz AnyPrinteriçin yapamayız Printer<T>. Bunun yerine, _printtabanın fonksiyonuna bir fonksiyon göstericisi elde ederiz print. Çağrılmadan çağrı base.printyapmak, temelin öz değişkeni olarak kıvrıldığı bir işlev döndürür ve bu nedenle gelecekteki çağrılar için korunur.

Akılda tutulması gereken bir başka nokta da, bu çözümün esasen başka bir dinamik dağıtım katmanı olmasıdır, bu da performansa hafif bir darbe anlamına gelir. Ayrıca, tür silme örneği, temel alınan örneğin üzerinde fazladan bellek gerektirir. Bu nedenlerden dolayı, tür silme, ücretsiz bir soyutlama değildir.

Açıkçası, tür silmeyi ayarlamak için bazı çalışmalar var, ancak genel protokol soyutlaması gerekiyorsa çok yararlı olabilir. Bu desen, hızlı standart kitaplıkta gibi türlerle bulunur AnySequence. Daha fazla okuma: http://robnapier.net/erasure

BONUS:

Her Printeryerde aynı uygulamayı enjekte etmek istediğinize karar verirseniz , AnyPrintero türü enjekte eden bir kolaylık başlatıcı sağlayabilirsiniz .

extension AnyPrinter {

    convenience init() {

        let nsLogger = NSLogger<T>()

        self.init(base: nsLogger)
    }
}

let printer = AnyPrinter<Int>()

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

Bu, uygulamanızda kullandığınız protokoller için bağımlılık enjeksiyonlarını ifade etmenin kolay ve KURU bir yolu olabilir.


Bunun için teşekkürler. Bu tür silme modelini (işlev işaretçileri kullanarak fatalError()), diğer tür silme öğreticilerinde açıklanan soyut bir sınıf (tabii ki mevcut olmayan ve kullanılarak taklit edilmesi gereken ) kullanmaktan daha çok seviyorum .
Chase

4

Güncellenmiş kullanım durumunuzu ele almak:

(btw Printablezaten standart bir Swift protokolüdür, bu nedenle kafa karışıklığını önlemek için muhtemelen farklı bir ad seçmek isteyeceksiniz)

Protokol uygulayıcıları üzerinde belirli kısıtlamalar uygulamak için, protokolün tür takma adlarını kısıtlayabilirsiniz. Öyleyse, öğelerin yazdırılabilir olmasını gerektiren protokol koleksiyonunuzu oluşturmak için:

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

Şimdi, yalnızca yazdırılabilir öğeler içeren bir koleksiyon uygulamak istiyorsanız:

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

Ancak, mevcut Swift koleksiyon yapılarını bu şekilde kısıtlayamayacağınızdan, sadece uyguladıklarınızı kısıtlayamayacağınız için bu muhtemelen çok az gerçek bir yardımcıdır.

Bunun yerine, girdilerini yazdırılabilir öğeler içeren koleksiyonlarla sınırlayan genel işlevler oluşturmalısınız.

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

Oh adamım bu iğrenç görünüyor. İhtiyacım olan şey, genel destekle bir protokol olması. Bunun gibi bir şeye sahip olmayı umuyordum: protocol Collection <T>: SequenceType. Ve bu kadar. Kod örnekleri için teşekkürler, sanırım onu ​​sindirmenin biraz zaman alacağını düşünüyorum :)
Tamerlane
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.