일반 프로토콜을 변수 유형으로 사용하는 방법


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
다음은 2014 년 Apple Developer Forums의 관련 스레드입니다.이 질문은 Apple의 Swift 개발자가 (어느 정도) 해결했습니다. devforums.apple.com/thread/230611 (참고 :이 항목을 보려면 Apple 개발자 계정이 필요합니다. 페이지).
titaniumdecoy

답변:


88

Thomas가 지적했듯이, 유형을 전혀 제공하지 않음으로써 변수를 선언 할 수 있습니다 (또는 명시 적으로 type으로 지정할 수 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)

컴파일 타임에 컴파일러 printStoredValueInts 용과 s 용 의 두 가지 버전을 작성하는 것처럼 괜찮습니다 String. 이 두 버전 내에서은 x특정 유형으로 알려져 있습니다.


20
즉, 매개 변수로 제네릭 프로토콜을 가질 수있는 방법이 없으며 그 이유는 Swift가 제네릭의 .NET 스타일 런타임 지원을 지원하지 않기 때문입니다. 이것은 매우 불편합니다.
Tamerlane 2014

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

42

이 질문에 대해 언급되지 않은 해결책이 하나 더 있는데, 유형 삭제 라는 기술을 사용합니다 . 일반 프로토콜에 대한 추상 인터페이스를 얻으려면 프로토콜을 준수하는 객체 또는 구조체를 래핑하는 클래스 또는 구조체를 만듭니다. 일반적으로 '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>. 대신 우리는 _printbase의 print함수에 대한 함수 포인터 를 얻습니다 . 호출 base.print하지 않고 호출하면 base가 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 프로토콜이므로 혼동을 피하기 위해 다른 이름을 선택하고 싶을 것입니다)

프로토콜 구현 자에 대한 특정 제한을 적용하려면 프로토콜의 typealias를 제한 할 수 있습니다. 따라서 요소를 인쇄 할 수 있어야하는 프로토콜 컬렉션을 만들려면 :

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