自分の理解
- some: 関数やComputed Propertiesの返り値で使用。処理の中で1種類のTypeを返したい & 呼び出し元ではそのTypeのProtocolで返り値を扱いたい時に使う。
- any: existentialである定義をわかりやすくするための書き方
someは多分packageの境界とかで使用するかもなあ。
anyもswift6から必須になるらしいので、今のうちにつけといた方が良いのかも。
なぜsome、anyがあるのか
existentialとは
existentialであるとは、参考のサイトを見る限りでは定義にProtocolが使用されているものを指すらしい。
protocol Product {
var id: Int { get }
var name: String { get }
var price: Int { get }
func compareString(product: Self) -> String
}
let product: Product // <- existential
genericsとexistentialの違い
簡単にいうと抽象化した型の推測が効くか聞かないか。
以下の例を見てみる。(あまり良い例ではない気がするので、もし他に良き例が思い付いたらそれにしたい。。。)
protocol Product {
var id: Int { get }
var name: String { get }
var price: Int { get }
init(id: Int, name: String, price: Int)
}
struct SmartPhone: Product {
private(set) var id: Int
private(set) var name: String
private(set) var price: Int
private(set) var isIPhone: Bool = false
init(id: Int, name: String, price: Int) {
self.id = id
self.name = name
self.price = price
}
mutating func setIsIPhone(_ value: Bool) {
isIPhone = value
}
}
func existentialProduct(product: Product) -> Product {
return product
}
func updateProductPrice<P: Product>(product: P, price: Int) -> P {
P.init(id: product.id, name: product.name, price: product.price)
}
let phone = SmartPhone(id: 1, name: "iphone se", price: 50000)
let existentialPhone = existentialProduct(product: phone)
existentialPhone.setIsIPhone(true)
var updatedProduct = updateProductPrice(product: phone, price: 100000)
updatedProduct.setIsIPhone(true)
productというprotocolと、それを継承したSmartPhoneというstructがある。
SmartPhoneのinstanceを作成し、それを
- existentialProduct(product: Product) -> Product ……(1)
- updateProductPrice(product: P, price: Int) -> P ……(2)
という命令にかけて見ている。この時、(1)の引数、返り値はexistentialである。
この際、(1)の方は返り値がそのままProductとして返ってくるが、
(2)を使用した場合にはPの型が引数のproduct: P
から推測されて、SmartPhoneとわかり、返り値の方もSmartPhoneで帰ってくる。
実際、
- existentialPhone.setIsIPhone(true)
はエラーとなる。
existensialの型のプロパティにアクセス
func existentialProduct(product: Product) -> Product
の内部でproductのプロパティにアクセスしようとすると、extentialであり、型の推測ができず、実際の値はわからない。
なので、ランタイム時にProduct protocoのデータが格納されているメモリから、実際のSmartPhoneのデータが格納されているメモリへの間接アクセスをする必要があり、少しパフォーマンスが悪い。
対して、
updateProductPrice(product: P, price: Int) -> P
はコンパイル時に型が推測されるため、ランタイム時には実データへ直接アクセスするので、こちらの方がパフォーマンスが良い。
なので、必要がなければ、なるべくprotocolよりgenericsを使用した方が良い、ということになる。
any
こちらは簡単で、単に、existentialな定義をわかりやすくするための表現。
なので、(1)は
func existentialProduct(product: any Product) -> any Product {
return product
}
のように書ける。
some
以下の例を通して見ていく。
protocol Product {
var id: Int { get }
var name: String { get }
var price: Int { get }
func compareString(product: Self) -> String
}
struct SmartPhone: Product {
private(set) var id: Int
private(set) var name: String
private(set) var price: Int
func compareString(product: SmartPhone) -> String {
let nameCompare = "\(product.name) vs \(name)"
let priceCompare = "price: \(product.price) --- \(price)"
return "\(nameCompare) \n \(priceCompare)"
}
}
func createSmartPhone(id: Int, name: String, price: Int) -> SmartPhone {
SmartPhone(id: id, name: name, price: price)
}
以上の例ではSmartPhoneのinstanceを返すような関数を作成した。
仮定として、この関数がパッケージに入っていて、その外部からこの関数を利用したい、とする。
この時、SmartPhoneでは具体的なクラスがわかってしまうので、なるべくならば隠蔽したい。
パッと思いつくのは以下のような返り値をProductにしてしまうことだが、エラーが出てしまう。
func createSmartPhone(id: Int, name: String, price: Int) -> Product {
SmartPhone(id: id, name: name, price: price)
}
なぜか。
Product protocolの関数中で、Self
を引数として使用しているから。
同様にProtocolがassociatedType
を使用していたりすると、ここで使用することができない。
また、この様に返せたとして、返り値がexistentialであり、少しパフォーマンスが落ちる。
この様な時にsomeを使用する。
func createSmartPhone(id: Int, name: String, price: Int) -> some Product {
SmartPhone(id: id, name: name, price: price)
}
この時、呼び出し側では「Productに準拠した何かしらの具体的なクラス」として扱われ、具体クラスを返り値とした時と変わらないパフォーマンスが出せる。また、Selfの問題も無視できる。
ただ、縛りがあり、
- someを返り値とする関数やComputed Propertyの返り値は一意でならなければならない。
という違いがあることに注意する。
なので、この関数の中でProductに準拠したSmartPhone以外のクラスのinstanceを返すことはできない。
このように、someは「一つしか返すクラスの種類はないが、クラス情報を隠蔽したい」ときに使用する。
(推測)なぜsomeで複数個のクラスが返せないか
あくまで推測です。記事あったら教えてください。。。
比較をする際に矛盾が生じるからではないかと推測している。
複数個具体的なクラスを返せる、と仮定してみる。
some Product
は何かしらの具体的であり、Productを継承したクラスを返したいが、関数自体は具体的なクラスを1つしか指定して返すことができない。
そこで、複数個のクラスを返す代わりに、Productを継承したSomeProduct
クラスをコンパイルしているときに作成し、代わりに返す様なことが必要になる。
let flag = true
func _createProduct(id: Int, name: String, price: Int) -> some Product {
if flag {
return SmartPhone(id: id, name: name, price: price)
} else {
return Instrument(id: id, name: name, price: price)
}
}
といったものがあった際、コンパイルすると、
let flag = true
func _createProduct(id: Int, name: String, price: Int) -> SomeProduct {
SomeProduct(id: id, name: name, price: price)
}
といった様に解釈される必要がある。
この時、以下の様な時にコンパイル前と矛盾が起きる。
let flag = true
func _createProduct(id: Int, name: String, price: Int) -> SomeProduct {
SomeProduct(id: id, name: name, price: price)
/*コンパイル前
if flag {
return SmartPhone(id: id, name: name, price: price)
} else {
return Instrument(id: id, name: name, price: price)
}
*/
}
// SmartPhoneクラスのつもり
let a = _createProduct(id: 1, name: "iPhone", price: 100000)
let flag = false
// Instrumentクラスのつもり
let b = _createProduct(id: 1, name: "iPhone", price: 100000)
// ただ、コンパイルされると、a,bのどちらもSomeProductクラスと見做されて、
// ここがTrueになってしまい、矛盾
type(of: a) == type(of: b)