it-swarm-ja.com

Swiftの構造とクラス

構造体がクラスよりもパフォーマンスが高くなる可能性があると述べられている理由の1つは、構造体にARCが必要ないことです。しかし、Swiftに次の構造体があるとします。

struct Point {
   var x:Float
   var y:Float
   mutating func scale(_ a:Float){
      x *= a
      y *= a
   } 
}

var p1 = Point(x:1, y:1)
var p2 = p1 //p1 and p2 point to the same data internally
p1.scale(2) //copy on mutate, p1 and p2 now have distinct copies

今私は、構造体のさまざまなコピーが、突然変異がコピーを強制するまで、実際にはスタック上の同じデータを指すと信じています。つまり、突然変異がコピーを形成する必要があるかどうかを知るために、スタック上の特定のメモリアドレスを参照するオブジェクトの数を追跡する必要があるか、単純にすべての突然変異にコピーを形成する必要があります。前者は非効率的で、後者はARCと同じようです。何が欠けていますか?

編集:私は値のセマンティクスと参照のセマンティクスの違いを理解しています。しかし、最適化としてSwiftは、変更が行われるまで、実際にはデータの新しいコピーを作成しません。これにより、不要なコピーが回避されます。参照 https://www.hackingwithswift.com/ example-code/language/what-is-copy-on-write ですが、これが原因で、何らかの形式のARCを回避できるかどうかはわかりません。この記事では、書き込み時のコピーは配列と辞書それが真実かどうかはわかりませんが、もしそうだとしても、私の質問はまだ配列についてです。

6
gloo

Swift構造体は、そのデータが一意に参照されているかどうかを知るだけでよく、本格的なARCを必要としません。たとえば、次の行で

var p2 = p1

コンパイラーは理論的には、p1の一意に参照されるフラグをオフからオンに切り替えるだけです。次に、書き込みが行われると、コンパイラはコピーすることを認識します。

これが発生する前にp2の割り当てが解除されたとします。現在、コピーは冗長ですが、コンパイラーは知らないので、それでもコピーを作成します。これが、一意に参照されるアプローチが完全な近似ではない理由です。実際には、完全なARCと常にコピーすることの間にあります。

注:Swiftコンパイラは実際にはこのように機能しません。@ Alexanderのコメントと this answer に基づいて、コピーオンライト動作はSwiftコードで実装されます。コンパイラの最適化。ユーザー定義の構造体では、毎回コピーするだけです。

効率についてのあなたの要点については、はい、技術的に毎回コピーすることは効率が良くありません。コピーと変更の動作がたくさんある場合、ある時点で、クラスに切り替えるだけでより効率的になります。そのため、Swiftには両方のオプションがあります。

3
JSquared

あなたは誤解から始めています。それは構造体です。構造体は値型です。 p2への割り当てにより、コピーが作成されます。 p1とp2はどこも指していません。

それがまさに構造体とクラスの違いです。クラスは参照型です。クラスがある場合、p2への割り当ては、p1が指す(および参照をカウントする)同じオブジェクトへの2番目の参照を作成するだけです。

「値の型の目的に反するポインタの間接参照を使用せずに、値の型がCOWを実行する方法はありません」と言っています。値型の目的は、値型のセマンティクスです。実装の詳細は関係ありません。

しかし、さらに先に進む必要があります。 2つのFloatを含む構造体をコピーすると、2つのFloatがコピーされます。オブジェクトへの2つの参照を含む構造体をコピーすると、オブジェクト参照がコピーされます-これで、同じオブジェクトへの参照を持つ2つの構造体ができました。コピー内の参照を置き換える場合、その元の構造体は変更されません。コピー内の参照を使用してオブジェクトを変更すると、それは元のオブジェクトによって参照されるオブジェクトと同じであるため、元の構造体によって参照されるオブジェクトも同じオブジェクトであり、変更されています。

配列は構造体ですが、構造体には実際のデータを含むオブジェクトへの参照が含まれています(これはSwiftライブラリの配列の実装の実装の詳細です)。この参照はコピーされるので、この時点でコピーされた配列はまったく同じデータオブジェクトを参照します。誰も配列を変更しようとしない限り問題ありません。そのデータオブジェクトが変更されると、アクセサはデータオブジェクトへの参照の数をチェックし、複数ある場合は、次に、その時点でデータオブジェクトのコピーが作成されます。

配列の観察可能な動作は、配列構造体がコピーされたときにすべてのアイテムがコピーされたかのようです。しかし、一部の巧妙なコードは実際に必要なときにのみデータをコピーするため、実装ははるかに高速です。独自の構造体に対して同じ最適化が必要な場合は、それを行うためのコードを記述する必要があります。コンパイラとは何の関係もありません。すべてSwiftライブラリの配列実装です。

4
gnasher729

@amonのリンクは完全な回答です。宛先 引用

値型は、変数または定数に割り当てられたとき、または関数に渡されたときに値がコピーされる型です。

クラスとは対照的に

値型とは異なり、参照型は、変数または定数に割り当てられたとき、または関数に渡されたときにコピーされません。コピーではなく、同じ既存のインスタンスへの参照が代わりに使用されます。

構造体タイプの値によるコピーのセマンティクスは、主に参照によるコピーのみである言語に精通しているユーザーにトリップする傾向があります。この振る舞いを本当に理解するには、Cを見る価値があります(SwiftはObjective-C自体にCに基づいて構築されています)。基本は Cリファレンス にあります=。

object:実行環境のデータストレージの領域。その内容は値を表すことができます。

構造タイプは、順次割り当てられた空ではないメンバーオブジェクトのセット(および、特定の状況では、不完全な配列)を記述し、それぞれにオプションで指定された名前がありますそしておそらく異なるタイプ。

単純な代入(=)では、右のオペランドの値は代入式の型に変換され、指定されたオブジェクトに格納されている値を置き換えます左のオペランド

これら3つの定義をまとめると、C structsがコピーオンライトではない理由がわかります。 Cで持っているなら

struct foo {};

bar() {
    struct foo a = <whatever>;
    struct foo b = <whatever>;
    b = a;
}

この場合、aおよびbはデータストレージの領域の値です(注:これらの領域への参照やポインターではなく、それらの領域の値)。表現 b = aは、bの領域に格納されている値をaの領域の値に置き換えます。つまり、aは「コピー」され、bに割り当てられます。

したがって、ここでのCの動作は、その仕様と抽象マシンモデル(標準ハードウェアでの効率的な実装に非常に対応できるように設計されています)の自然な結果です。 Objective Cはこの動作を継承し、SwiftはObjective Cをフォローしています。

3
walpen