迅速な機能

この記事では、最初に会ったときに出会ったSwiftの機能と困難についてお話ししたかったのです。 記事を書くために、言語バージョン2.0が使用されました。 ドキュメントをすでに読んでおり、モバイルアプリケーションの開発に関する基本的な知識があることを前提としています。

汎用プロトコル


この用語では、オープンタイプエイリアス(Swift 2.2の関連タイプ)を持つプロトコルを意味します。 私の最初のSwiftアプリケーションには、これらのプロトコルが2つありました(たとえば、それらを少し単純化しました)。

protocol DataObserver { typealias DataType func didDataChangedNotification(data: DataType) } protocol DataObservable { typealias DataType func observeData<TObserver: DataObserver where TObserver.DataType == DataType> (observer: TObserver) } 

DataObservableは、データの変更を追跡する責任があります。 このデータがどこに保存されているかは問題ではありません(サーバー上、ローカル、その他何でも)。 DataObserverは、データが変更されたというアラートを受け取ります。 まず、DataObservableプロトコルに興味があります。これが最も簡単な実装です。

 class SimpleDataObservable<TData> : DataObservable { typealias DataType = TData private var observer: DataObserver? var data: DataType { didSet { observer?.didDataChangedNotification(data) } } init(data: TData) { self.data = data } func observeData<TObserver : DataObserver where TObserver.DataType == DataType>(observer: TObserver) { self.observer = observer } } 

ここではすべてが簡単です。最後のオブザーバーへのリンクを保存し、何らかの理由でデータが変更されたときに、そのリンクでdidDataChangedNotificationメソッドを呼び出します。 しかし、ちょっと...このコードはコンパイルされません。 コンパイラは、「Protocol 'DataObserver'はSelfまたは関連する型の要件があるため、汎用制約としてのみ使用できます」というエラーをスローします。 これは、汎用プロトコルは汎用パラメータに制限を課すためにのみ使用できるためです。 つまり タイプDataObserverの変数の宣言は失敗します。 この状況は私には向いていませんでした。 少し調べてみると、現在の問題に対処するのに役立つ解決策が見つかりました。彼の名前はType Erasureです。

これは、特定のプロトコルの小さなラッパーであるパターンです。 最初に、DataObserverプロトコルを実装する新しいAnyDataObserverクラスを紹介しましょう。

 class AnyDataObserver<TData> : DataObserver { typealias DataType = TData func didDataChangedNotification(data: DataType) { } } 

didDataChangedNotificationメソッドの本体は、今のところ空のままです。 どうぞ 一般的なinitクラスを紹介します(必要なものについては、以下で少し説明します)。

 class AnyDataObserver<TData> : DataObserver { typealias DataType = TData func didDataChangedNotification(data: DataType) { } init<TObserver : DataObserver where TObserver.DataType == DataType>(sourceObserver: TObserver) { } } 

タイプTObserverのsourceObserverパラメーターが渡されます。 TObserverに制限が課されていることがわかります。まず、DataObserverプロトコルを実装する必要があり、次に、そのDataTypeがクラスのDataTypeと完全に一致する必要があります。 実際、sourceObserverはこれがラップしたい元のオブザーバーオブジェクトです。 そして最後に、最終的なクラスコード:

 class AnyDataObserver<TData> : DataObserver { typealias DataType = TData private let observerHandler: TData -> Void func didDataChangedNotification(data: DataType) { observerHandler(data) } init<TObserver : DataObserver where TObserver.DataType == DataType>(sourceObserver: TObserver) { observerHandler = sourceObserver.didDataChangedNotification } } 

実際、ここですべての「魔法」が発生します。 プライベートobserverHandlerフィールドがクラスに追加され、sourceObserverオブジェクトのdidDataChangedNotificationメソッドの実装が保存されます。 クラス自体のdidDataChangedNotificationメソッドでは、この実装を単に呼び出します。

次にSimpleDataObservableを書き換えます。

 class SimpleDataObservable<TData> : DataObservable { typealias DataType = TData private var observer: AnyDataObserver<DataType>? var data: DataType { didSet { observer?.didDataChangedNotification(data) } } init(data: TData) { self.data = data } func observeData<TObserver : DataObserver where TObserver.DataType == DataType>(observer: TObserver) { self.observer = AnyDataObserver(sourceObserver: observer) } } 

これで、コードがコンパイルされ、うまく機能します。 Swift標準ライブラリの一部のクラスは、同様の原則(AnySequenceなど)で動作することに注意できます。

セルフタイプ


ある時点で、プロジェクトにコピープロトコルを入力する必要がありました。

 protocol CopyableType { func copy() -> ??? } 


しかし、copyメソッドは何を返す必要がありますか? 何か? CopyableType? 次に、呼び出しごとにlet copyObject = someObject.copy asを記述する必要があります! SomeClass、これはあまり良くありません。 さらに、このコードは安全ではありません。 キーワードSelfが助けになります。

 protocol CopyableType { func copy() -> Self } 


したがって、このメソッドの実装は、呼び出されたオブジェクトと同じ型のオブジェクトを返す必要があることをコンパイラに通知します。 ここで、Objective-Cのinstancetypeから類推できます。

このプロトコルの実装を検討してください。

 class CopyableClass: CopyableType { var fieldA = 0 var fieldB = "Field" required init() { } func copy() -> Self { let copy = self.dynamicType.init() copy.fieldA = fieldA copy.fieldB = fieldB return copy } } 


新しいインスタンスを作成するには、dynamicTypeキーワードを使用します。 オブジェクト型への参照を取得するのに役立ちます(これはすべてObjective-Cのクラスメソッドに似ています)。 型オブジェクトを受け取った後、initが呼び出されます(パラメーターのないinitが実際にクラス内にあることを確認するために、必要なキーワードとともに入力します)。 その後、必要なすべてのフィールドを作成されたインスタンスにコピーし、関数から返します。

コピーが終了するとすぐに、もう1つの場所でSelfを使用する必要がありました。 View Controller用のプロトコルを作成する必要がありました。このプロトコルには、このView Controller自体の新しいインスタンスを作成する静的メソッドがあります。

このプロトコルはUIViewControllerクラスに直接接続されていないため、非常に一般的なものにしてAutofactoryTypeを呼び出しました。

 protocol AutofactoryType { static func createInstance() -> Self } 


それを使用して、View Conotrollerを作成してみましょう。

 class ViewController: UIViewController, AutofactoryType { static func createInstance() -> Self { let newInstance = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("ViewController") return newInstance as! ViewController } } 

すべては問題ありませんが、このコードはコンパイルされません。「ViewController型の戻り式を戻り型 'Self'に変換できません」という事実は、コンパイラがViewControllerをSelfに変換できないことです。 この場合、ViewControllerとSelfは同じものですが、一般的なケースではそうではありません(たとえば、継承を使用する場合)。

このコードを機能させる方法は? このために、完全な正直さ(厳密なタイピングに関して)がありませんが、かなり有効な方法があります。 機能を追加:

 func unsafeCast<T, E>(sourceValue: T) -> E { if let castedValue = sourceValue as? E { return castedValue } fatalError("Unsafe casting value \(sourceValue) to type \(E.self) failed") } 

その目的は、あるタイプのオブジェクトを別のタイプに変換することです。 変換が失敗すると、関数は単に失敗します。

createInstanceでこの関数を使用します。

 class ViewController: UIViewController, AutofactoryType { static func createInstance() -> Self { let newInstance = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("ViewController") return unsafeCast(newInstance) } } 

自動型推論のおかげで、newInstanceはSelfに変換されるようになりました(直接実行できませんでした)。 このコードはコンパイルして動作します。

特定の拡張子


Swiftの型拡張は、異なる型の特定のコードを記述できなかった場合、それほど役に立ちません。 たとえば、標準ライブラリのSequenceTypeプロトコルを使用して、そのような拡張機能を記述します。

 extension SequenceType where Generator.Element == String { func concat() -> String { var result = String() for value in self { result += value } return result } } 

拡張では、シーケンスの要素に制限が導入されます;それはString型でなければなりません。 したがって、文字列で構成されるシーケンス(および文字列のみ)については、concat関数を呼び出すことができます。

 func test() { let strings = [“Alpha”, “Beta”, “Gamma”] //printing “AlphaBetaGamma” print("Strings concat: \(strings.concat())") } 

これにより、コードの大部分を拡張機能に配置し、適切なコンテキストで呼び出すことができ、再利用のすべての利点を享受できます。

プロトコルのデフォルトメソッドを実装します。


プロトコルのデフォルトメソッドを実装します。

 protocol UniqueIdentifierProvider { static var uniqueId: String { get } } 

説明からわかるように、このプロトコルを実装する型には、String型の一意の識別子uniqueIdが必要です。 しかし、少し考えてみると、あらゆるタイプの1つのモジュールのフレームワーク内で、その名前が一意の識別子であることが明らかになります。 それでは、新しいプロトコルの拡張機能を作成しましょう。

 extension UniqueIdentifierProvider where Self: UIViewController { static var uniqueId: String { get { return String(self) } } } 

この場合、Selfキーワードを使用して、タイプオブジェクトに制限を課します。 このコードのロジックはほぼ次のとおりです。「このプロトコルがUIViewControllerクラス(またはその子孫)によって実装されている場合、次のuniqueId実装を使用できます。 これはデフォルトのプロトコル実装です。 実際、この拡張機能は制限なしで作成できます。

 extension UniqueIdentifierProvider { static var uniqueId: String { get { return String(self) } } } 

そして、UniqueIdentifierProviderを実装するすべてのタイプは、すぐにuniqueIdを取得します。

 extension ViewController: UniqueIdentifierProvider { //Nothing } func test() { //printing "ViewController" print(ViewController.uniqueId) } 

美しさは、クラスがこのメソッドの独自の実装を持つことができることです。 この場合、デフォルトの実装は無視されます。

 extension ViewController: UniqueIdentifierProvider { static var uniqueId: String { get { return "I'm ViewController” } } } func test() { //printing "I'm ViewController" print(ViewController.uniqueId) } 

明示的なジェネリック引数


私のプロジェクトでは、MVVMを使用し、メソッドがViewModelの作成を担当しました。

 func createViewModel<TViewModel: ViewModelType>() -> TViewModel { let viewModel = TViewModel.createIntsance() //View model configurate return viewModel } 


したがって、次のように使用されました。

 func test() { let viewModel: MyViewModel = createViewModel() } 

この場合、MyViewModelは汎用引数としてcreateViewModel関数に提供されます。 これは、Swift自体がコンテキストから型を取り出すためです。 しかし、それは常に良いですか? 私の意見では、そうではありません。 場合によっては、エラーが発生することもあります。

 func test(mode: FactoryMode) -> ViewModelBase { switch mode { case NormalMode: return createViewModel() as NormalViewModel case PreviewMode: return createViewModel() // as PreviewViewModel } } 


最初の場合、NormalViewModelはcreateViewModelメソッドに置き換えられます。
2つ目は、「PreviewViewModelとして」の記述を忘れていたため、ViewModelBase型がcreateViewModelメソッドに置き換えられる理由です(最良の場合、実行時にエラーが発生します)。

したがって、型の指示を明示的にする必要があります。 これを行うには、createViewModelで、TViewModel.Type型の新しいパラメーターviewModelTypeを追加します。 ここでの型は、メソッドが型のインスタンスをパラメーターとして受け入れず、型オブジェクト自体を受け入れることを意味します。

 func createViewModel<TViewModel: ViewModelType>(viewModelType: TViewModel.Type) -> TViewModel { let viewModel = viewModelType.createIntsance() //View model configurate return viewModel } 

その後、スイッチケースは次のようになります。

 func test(mode: FactoryMode) { let viewModel: ViewModelBase? switch mode { case NormalMode: return createViewModel(NormalViewModel.self) case PreviewMode: return createViewModel(PreviewViewModel.self) } } 

これで、引数NormalViewModel.selfとPreviewViewModel.selfがcreateViewModel関数に渡されます。 これらは、NormalViewModelおよびPreviewViewModel型オブジェクトです。 Swiftにはかなり奇妙な機能があります。関数にパラメーターが1つある場合、selfをスキップできます。

 func test(mode: FactoryMode) { let viewModel: ViewModelBase? switch mode { case NormalMode: return createViewModel(NormalViewModel) case PreviewMode: return createViewModel(PreviewViewModel) } } 


ただし、2つ以上の引数がある場合は、キーワードselfが必要です。

PS


この記事が誰かに役立つことを願っています。 また、Swiftについても継続する予定です(これだけではありません)。

Source: https://habr.com/ru/post/J306350/


All Articles