protocolは準拠していれば、たとえ型が違くてもprotocolに準拠した処理を行えるといった点で便利な反面、
- protocolに準拠していればどんな値でも代入可能になってしまう
- protocolへの準拠が非効率的なされることで、メモリを余分に消費してしまう
といったデメリットも存在します。そうしたデメリットを解消するためにSwiftに新しく追加されたのが、anyやsomeといった文法です。
ここでは、それぞれの特徴や使い分けについて紹介していきます。
anyの使い道
anyを使うべき場合
1. 型定義としてprotocolを用いる場合
例えば、以下のようなコードの場合を考えます。
protocol Food {
var name: String { get }
var weight: Int { get }
}
struct Vegetable: Food {
var name: String
var weight: Int
}
private let tomato: any Food = Vegetable(name: "Tomato",
weight: 100)
このように型定義としてprotocolを用いる場合、anyを明示的につけることで、ある変数にprotocolへ準拠した値が入ることを知らせることができます。
(この場合はanyがついてなくてもコンパイルは通りますが、以下のようにassociatedtypeがあった場合、エラーとなります。)
protocol Food {
associatedtype From
var name: String { get }
var weight: Int { get }
var from: From { get }
}
struct Vegetable: Food {
var name: String
var weight: Int
var from: String
}
// コンパイルエラー: Use of protocol 'Food' as a type must be written 'any Food'
private let tomato: Food = Vegetable(name: "Tomato",
weight: 100,
from: "Tokyo")
2. 複数の型に対応したメソッドを定義する時
protocolに準拠している型が複数存在する場合、それらに共通して使えるメソッドを作りたい場合があります。
その場合、anyを使うことでそのメソッドを定義できます。
例えば、以下のように合計値を出すメソッドをCollectionに準拠するどのプロトコルにも使用したいと言う場合です。
func sum(data: any Collection<Int>) -> Int {
return data.reduce(0) {$0 + $1}
}
let dataA = Array(1...5)
let dataB = Set(1...5)
let dataC = dataA.reversed()
let data: [any Collection<Int>] = [dataA, dataB, dataC]
for datum in data {
print(sum(data: datum)) // 15, 15, 15
}
Collectionに準拠する場合にはreduceが使えるので、引数の型指定の部分にanyを付与することで望み通りの関数を作ることができます。
またdataのように、同じprotocolに準拠した複数の型を同時に入れるArrayを作りたい時にもanyを使うことでコンパイルさせることができます。
余談
Collectionに関して言うと、any Collectionに相当する構造体であるAnyCollectionがSwiftでは定義されています。
let data: [AnyCollection<Int>] = [AnyCollection(dataA), AnyCollection(dataB), AnyCollection(dataC)]
for datum in data {
print(sum(data: datum)) // 15, 15, 15
}
こちらでも全く同じ結果を得ることができます。AnyCollectionはSequenceにも準拠しているのでflatMapなどany Collectionには使えないメソッドを使うことができます。
associatedtypeについて
associatedtypeの利点は、型の制約を受けずに処理を記述できる点です。
FromはStringやInt等、何でも入れることができます。
制約を設けたい場合には、以下のように記述すれば大丈夫です。
protocol Food {
// 制約を追加
associatedtype From: Prefecture
var name: String { get }
var weight: Int { get }
var from: From { get }
}
struct Vegetable: Food {
var name: String
var weight: Int
var from: Pref
}
protocol Prefecture {
var name: String { get }
}
enum Pref: Prefecture {
case tokyo
case other
var name: String {
switch self {
case .tokyo:
return "Tokyo"
case .other:
return "Not Tokyo"
}
}
}
private let tomato: any Food = Vegetable(name: "Tomato",
weight: 100,
from: .tokyo)
// コンパイルエラー
private let carrot: any Food = Vegetable(name: "Tomato",
weight: 100,
from: "Osaka")
anyの制限
また、anyを使用していると、準拠しているものであれば何でも指定できてしまいます。
struct Meat: Food {
var name: String
var weight: Int
var from: Pref
}
private let beef = Meat(name: "Beef",
weight: 100,
from: .other)
protocol Store {
var items: [any Food] { get }
func register()
}
struct VegetableStore: Store {
var items: [any Food] = []
func register() {
items.forEach {
print($0.name)
print($0.from.name)
}
}
}
var vegetableStore = VegetableStore()
vegetableStore.items = [tomato, beef]
vegetableStore.register()
// Tomato
// Tokyo
// Beef
// Not Tokyo
例えば上のようにVegetableであるFoodに限定したい場合、以下のようにジェネリクスを指定してあげることで、使用できる型を狭めることができます。
protocol Store {
associatedtype FoodType: Food
var items: [FoodType] { get }
func register()
}
struct VegetableStore<T: Food>: Store {
var items: [T] = []
func register() {
items.forEach {
print($0.name)
print($0.from.name)
}
}
}
var vegetableStore = VegetableStore<Vegetable>()
// コンパイルエラー:Cannot convert value of type 'Meat' to expected element type 'Array<Vegetable>.ArrayLiteralElement' (aka 'Vegetable')
vegetableStore.items = [tomato, beef]
vegetableStore.register()
primary associated value
また、特定の型のオブジェクトに関する処理を記述したい場合の便利な書き方として、primary associated valueが挙げられます。
以下のようにprotocol自体にジェネリクスを定義することで、関数の型制約の部分でジェネリクスにより型制限を行うことができます。
protocol Store<FoodType> {
associatedtype FoodType: Food
var items: [FoodType] { get }
func register()
}
func buy(_ store: any Store<Vegetable>) -> Vegetable {
return store.items[0]
}
someの使い道
someもanyと同じく、型定義としてprotocolを用いるときに使われますが、一番の大きな違いは、コンパイル後の挙動です。
anyがprotocolに準拠しているものであればコンパイル後でも何の型でも入るのに対し、someは異なる型を入れようとするとコンパイルエラーになると言う点です。someの場合、Arrayの中の要素も全て型が揃っていないといけません。
let dataA = Array(1...5)
let dataB = Set(1...5)
let dataC = dataA.reversed()
let data: [any Collection<Int>] = [dataA,dataB,dataC]
let data2: [some Collection<Int>] = [dataA,dataB,dataC] // コンパイルエラー: Conflicting arguments to generic parameter 'τ_0_0' ('[Int]' vs. 'Set<Int>' vs. 'ReversedCollection<[Int]>')
var dataAAny: any Collection = dataA
var dataASome: some Collection = dataA
dataAAny = dataB
// コンパイルエラー:Cannot assign value of type 'Set<Int>' to type '[Int]
dataASome = dataB
また、型の制約としてprotocolを使う場合も、引数の型が一定になるという点でanyよりもsomeの方が望ましいです。
// 修正
func buy(_ store: some Store<Vegetable>) -> Vegetable {
return store.items[0]
}
このように、someを使うと型の制約が強くなります。そのため、より安全なコードを書くためにはanyを使えそうなところは極力someに置き換える、といった認識が大事でしょう。
余談
SwiftUIでViewはvar body: some Viewから構成されています。これはViewに準拠していればどのようなViewの組み合わせでもコンパイル可能であり、何か特定の型で返すよりも非常に柔軟性が高く、かつエラーも生じにくい仕組みを使っていると言えます。
まとめ
- protocolは値の型定義や
associatedtypeの制約、値への代入の制約などに用いることができる- protocolを型定義や型の制約に用いる場合、
anyあるいはsomeを使用するべきである
- protocolを型定義や型の制約に用いる場合、
-
associatedtypeによって、同一のプロトコルに基づく複数の型に共通する処理を記述できる - primary associated typeによって、特定の型のみに絞った処理を記述できる
-
someとanyの一番の違いは、コンパイル時に型決定をするかしないか-
anyだとコンパイル後に別の型を代入したり、別の型からなるArrayを作成したりできるが、someはコンパイル時に型を決定し、同一の型の値しか代入、Arrayの作成を行えない
-
-
someの方が型的に安全であるため、極力someを使うのが望ましい。
最後に
こちらは私が書籍で学んだ学習内容をアウトプットしたものです。
わかりにくい点、間違っている点等ございましたら是非ご指摘お願いいたします。