inoutパラメーターご存知でしょうか?
私は、最近知りました。
少し調べてせっかくなので感想等、記録します。
値型(Value Types)と参照型(Reference Types)の違い
先ず、このinoutパラメーターを説明する前に値型、参照型についての理解が必要ですので、ライトな説明だけ挟みます。
値型と参照型は、プログラム内でデータがどのように扱われるかに関する基本的な概念です。
それぞれの違いを、ミュータントタートルズ世代としてピザに例えます。
値型(Value Types)
- 値型は、変数や定数が実際の値を持ちます。
- 値型をコピーすると、実際の値が複製されます。
- 例:Int、Float、String、Array、Dictionary、Set、Structなど
例: ピザのレシピをコピーして共有するイメージ。あなたと友達が同じピザを作りたいとします。あなたがマルゲリータピザのレシピを持っていて、それを友達にコピーして渡します。友達がレシピをペパロニに書き換えたとしても、あなたのレシピはマルゲリータのままで影響しません。
var myRecipe = "MargheritaRecipe"
var friendRecipe = myRecipe
friendRecipe = "PepperoniRecipe"
print(myRecipe) // 出力: MargheritaRecipe
print(friendRecipe) // 出力: PepperoniRecipe
参照型(Reference Types)
- 参照型は、データへの参照を持つタイプです。
- 参照型を変数にコピーすると2つの変数がインスタンスの実体が存在するメモリ位置(アドレス)を共有していることになります。
- 例:Class、Function
例: 一枚のピザのレシピをシェアするイメージ。あなたと友達が同じレシピを見ながらピザを作ります。友達がレシピをペパロニピザに書き換えれば、二人ともペパロニピザを作ることになります。
class Pizza {
var pizzaKind: String
init(pizzaKind: String) {
self.pizzaKind = pizzaKind
}
}
var myPizza = Pizza(pizzaKind: "Margherita")
var friendPizza = myPizza
friendPizza.pizzaKind = "Pepperoni"
print(myPizza.pizzaKind) // 出力: Pepperoni
print(friendPizza.pizzaKind) // 出力: Pepperoni
inoutパラメーターの書き方
func swapValues<T>(a: inout T, b: inout T) {
let temp = a
a = b
b = temp
}
var x = 1
var y = 2
swapValues(a: &x, b: &y)
print("x: \(x), y: \(y)") // 出力: x: 2, y: 1
説明
-
関数宣言:
-
func swapValues<T>(a: inout T, b: inout T)
:inout
キーワードを使って、パラメーターa
とb
が関数内で変更され、その変更が呼び出し元に反映されるようにします。
-
-
関数呼び出し:
-
swapValues(a: &x, b: &y)
:&
記号を使って、変数x
とy
の参照を渡します。これにより、関数内での変更が呼び出し元に反映されます。
-
inout パラメーターの使用
参照渡しを使った変数の直接変更(外部に影響のある使用)
- 目的: 変数の値を直接変更する。
- 特徴: 関数が終了した後でも、呼び出し元の変数の変更が反映される。
func swapValues<T>(a: inout T, b: inout T) {
let temp = a
a = b
b = temp
}
var x = 1
var y = 2
swapValues(a: &x, b: &y)
print(x, y) // 出力: 2 1
内部でinoutを使用した関数の選択(関数内部にのみに影響)
- 目的: 関数内部での一時的なデータ操作を効率化し、パフォーマンスを向上させる。
- 特徴: 一時的なオブジェクトの生成を避け、メモリ効率が向上する。
let array = [1, 2, 3]
let sum = array.reduce(into: 0) { result, next in
result += next
}
print(sum) // 出力: 6
※以下で詳しい内部実装を書きます
特に有効と考える使用例:reduce関数内部においてのinoutパラメーターの活躍
※以下は@nak435さん、@loveeさんより頂いたアドバイスを元に作成しています。
reduce関数は2つあって、inout を使う reduce(into:) と使わない reduce(_:) があります。
1. inoutを使わない reduce(_:)の内部実装
public func reduce<Result>(
_ initialResult: Result,
_ nextPartialResult: (_ partialResult: Result, Element) throws -> Result
) rethrows -> Result {
var accumulator = initialResult
for element in self {
accumulator = try nextPartialResult(accumulator, element)
}
return accumulator
}
この実装では、nextPartialResult
クロージャが呼ばれるたびに、新しいResult
型の値が生成され、それがaccumulator
に代入されます。このため、各ステップで毎度新しいオブジェクトが生成されます。
2. reduce(into:_:)の内部実装
public func reduce<Result>(
into initialResult: Result,
_ updateAccumulatingResult: (_ result: inout Result, Element) throws -> ()
) rethrows -> Result {
var accumulator = initialResult
for element in self {
try updateAccumulatingResult(&accumulator, element)
}
return accumulator
}
この実装では、updateAccumulatingResult
クロージャがinout
パラメータを使用しているため、各ステップで新しいオブジェクトが生成されず、accumulator
の値が直接更新されます。
into initialResult: Result,
_ updateAccumulatingResult: (_ result: inout Result, Element) throws -> ()
reduce(into:)では初期値がinoutとして渡されているわけではなく、関数内でそのコピーがinoutとして利用されています。
let initialSum = 0 //参照渡しでないのでイミュータブルで問題ない
let array = [1, 2, 3]
let sum = array.reduce(into: initialSum) { result, next in
result += next
}
print(sum) // 出力: 6
print(initialSum) // 出力: 0 //参照渡しではないのでもちろん変化なし
2つのreduceの違い
-
reduce(::):
- 使い方: 関数の戻り値として新しい累積結果を返す。
- ポイント: 各ステップで新しい値が生成されるため、パフォーマンスの観点ではやや劣る。
-
reduce(into:_:):
- 使い方: inoutパラメータを使用して累積結果を直接更新する。
- ポイント: パフォーマンスが向上し、メモリ使用量が減る。特に大きなコレクションを扱う場合に有利。
2つのreduceの検証
ではどれぐらいのパフォーマンスの差があるのか簡単な方法でざっくりと検証します。
以下のような簡単な文字列連結を交互に実行して、実行時間とメモリ使用量の観点で結果を比べてみたいと思います。
正確なパフォーマンスはアプリケーションの実行環境やデータのサイズ、複雑性によって異ります
実行時間
let startTime = CFAbsoluteTimeGetCurrent()
let scrollWord_reduce = title.reduce("【\(selected)のお得情報】") { result, article in
result + article.article_title + " "
}.trimmingCharacters(in: .whitespaces)
// let scrollWord_reduce_into = title.reduce(into: "【\(selected)のお得情報】") { result, article in// 32768 bytes
// result += article.article_title + " "
// }.trimmingCharacters(in: .whitespaces)
let endTime = CFAbsoluteTimeGetCurrent()
//実行時間を出力
print("reduceTime: \(endTime - startTime)")
メモリ使用量
let memoryBefore = reportMemory()
let scrollWord_reduce = title.reduce("【\(selected)のお得情報】") { result, article in
result + article.article_title + " "
}.trimmingCharacters(in: .whitespaces)
// let scrollWord_reduce_into = title.reduce(into: "【\(selected)のお得情報】") { result, article in// 32768 bytes
// result += article.article_title + " "
// }.trimmingCharacters(in: .whitespaces)
let memoryAfter = reportMemory()
//メモリ使用量の差分を出力
print("Memory difference: \(memoryAfter - memoryBefore) bytes")
検証結果
実行時間(平均値) | メモリ使用量 (平均値) | |
---|---|---|
reduce(::) | 約0.000067353秒 | 49152 bytes |
reduce(into:_:) | 約0.000045856秒 | 32768 bytes |
reduce(into:)はreduce(:_:)に比べて 約31.9% ほど高速であり、約33.3% ほどメモリを節約できるため、より効率的であると言えそうです。
参照渡しを使った変数の直接変更例
1. 値のスワップ(交換)
複数の変数の値を一度に交換する場合
func swapValues<T>(a: inout T, b: inout T) {
let temp = a
a = b
b = temp
}
var x = 1
var y = 2
swapValues(a: &x, b: &y)
print("x: \(x), y: \(y)") // 出力: x: 2, y: 1
inoutを使わず同じことをすると
var x = 1
var y = 2
(x, y) = (y, x)
print("x: \(x), y: \(y)") // 出力: x: 2, y: 1
2. 複数の値の一括更新
一度に複数の値を更新する場合
func updateValues(a: inout Int, b: inout Int, c: inout Int) {
a += 1
b += 2
c += 3
}
var val1 = 10
var val2 = 20
var val3 = 30
updateValues(a: &val1, b: &val2, c: &val3)
print(val1, val2, val3) // 出力: 11 22 33
inoutを使わず同じことをすると
func updatedValues(a: Int, b: Int, c: Int) -> (Int, Int, Int) {
return (a + 1, b + 2, c + 3)
}
var val1 = 10
var val2 = 20
var val3 = 30
(val1, val2, val3) = updatedValues(a: val1, b: val2, c: val3)
print(val1, val2, val3) // 出力: 11 22 33
3. コレクションの要素の変更
関数内でコレクションの要素を変更する場合
func modifyArray(array: inout [Int]) {
for i in 0..<array.count {
array[i] += 1
}
}
var numbers = [1, 2, 3]
modifyArray(array: &numbers)
print(numbers) // 出力: [2, 3, 4]
inoutを使わず同じことをすると
func modifiedArray(array: [Int]) -> [Int] {
return array.map { $0 + 1 }
}
var numbers = [1, 2, 3]
numbers = modifiedArray(array: numbers)
print(numbers) // 出力: [2, 3, 4]
利点
-
関数内部のみでの使用:
- reduce(into:_:)の様に関数の内部で使う初期値にinoutパラメータを使う場合、既存の結果オブジェクトを直接変更するため、一時的なオブジェクトの生成を避けられ、パフォーマンスが向上します。
-
複数の値の一括操作:
- 複数の変数を一度に更新する場合に便利。
-
状態の一貫性:
- 関数内で一貫した操作を行うことで、値の整合性を保つことができます。
注意点
-
関数外への影響:
- inoutパラメーターは呼び出し元の変数を直接変更するため、慎重に使わないと予期しない副作用を引き起こす可能性があります。
-
コードの可読性:
- inoutパラメーターを多用すると、関数の呼び出し元のコードが分かりにくくなることがあります。
感想
Swiftのinoutパラメーターは適切に使うことでパフォーマンスやメモリ管理の向上に貢献します。ただ、inoutを使って外部変数を変更するのは注意が必要です。
関数が呼び出し元の変数を直接変更するため、再代入していない場合でも変数の値が変わる可能性があり、その原因を追跡するためには関数内部の実装を確認する必要があります。これにより、コードの可読性や保守性が低下するリスクがあると感じます。
一方で、reduce(into:_:)の様に関数内部のみでのinoutの使用は非常に効果的です。
inoutを利用することで新しいオブジェクトを繰り返し生成することなく、効率的にデータを集約できます。これにより、メモリ使用量を抑えつつ、パフォーマンスを向上させることができます。
結論として、inoutパラメーターは使いどころが大切です。関数外の変数を変更する用途には慎重な検討が必要ですが、時にinoutを使ったものを選択することには利点があります。