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
を使うのが望ましい。
最後に
こちらは私が書籍で学んだ学習内容をアウトプットしたものです。
わかりにくい点、間違っている点等ございましたら是非ご指摘お願いいたします。