Swift配列の割り当てに一貫性がない(参照もディープコピーもない)理由はありますか?


216

ドキュメントを読んでいて、言語の設計上の決定のいくつかに常に頭を悩ませています。しかし、本当に困惑したのは、配列の処理方法です。

私は遊び場に急いでこれらを試しました。あなたもそれらを試すことができます。だから最初の例:

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b

ここabは両方[1, 42, 3]とも受け入れられます。配列が参照されています-OK!

次の例をご覧ください。

var c = [1, 2, 3]
var d = c
c.append(42)
c
d

cis [1, 2, 3, 42]BUT dis [1, 2, 3]です。つまりd、前の例で変更を確認しましたが、この例では変更を確認していません。ドキュメントには、長さが変更されたためと記載されています。

さて、これはどうですか:

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e
f

e[4, 5, 3]、クールです。マルチインデックスの置換があると便利ですfが、長さが変更されていなくても、STILLは変更を認識しません。

つまり、1つの要素を変更すると配列への共通の参照で変更が表示されますが、複数の要素を変更したり項目を追加したりすると、コピーが作成されます。

これは私には非常に貧弱なデザインのようです。私はこれを考えるのは正しいですか?配列がこのように動作する理由が表示されない理由はありますか?

編集:配列が変更され、値のセマンティクスが追加されました。はるかに正気!


95
記録のために、私はこの質問が閉じられるべきではないと思います。Swiftは新しい言語であるため、私たち全員が学習するように、しばらくの間、このような質問があるでしょう。この質問は非常に興味深いものであり、誰かが抗弁について説得力のある事例を示すことを期待しています。
Joel Berger

4
@Joel Fine、プログラマーに聞いてください。StackOverflowは、特定の非固定化プログラミング問題のためのものです。
bjb568 2014

21
@ bjb568:それは意見ではありません。この質問は事実で答えられるべきです。Swift開発者が来て「X、Y、Zについても同じようにした」と答えた場合、それは事実です。X、Y、およびZに同意できない場合がありますが、X、Y、およびZについて決定が下された場合、それは言語の設計の歴史的な事実にすぎません。なぜstd::shared_ptr非アトミックバージョンがないのかと尋ねたときのように、意見ではなく事実に基づいて回答がありました(事実は、委員会が検討したが、さまざまな理由でそれを望まなかったという事実です)。
Cornstalks、2014

7
@JasonMArcher:最後の段落のみが意見に基づいています(そうですね、おそらく削除する必要があります)。質問の実際のタイトル(実際の質問そのものとして取り上げます)は、事実で回答可能です。そこの配列は、それらが動作するように動作するように設計された理由は。
Cornstalks 2014年

7
はい、API-Beastが言ったように、これは通常「Copy-on-Half-Assed-Language-Design」と呼ばれます。
R.マルティーニョフェルナンデス2014

回答:


109

配列のセマンティクスと構文はXcodeベータ3バージョンブログ投稿)で変更されたことに注意してください。次の回答はベータ2に適用されました。


これはパフォーマンス上の理由によるものです。基本的に、可能な限り配列のコピーを避けようとします(そして「Cのようなパフォーマンス」を主張します)。言語を引用するには:

配列の場合、コピーは配列の長さを変更する可能性のあるアクションを実行したときにのみ行われます。これには、アイテムの追加、挿入、削除、または範囲付き添え字を使用した配列内のアイテムの範囲の置き換えが含まれます。

これは少し混乱することに同意しますが、少なくともそれがどのように機能するかについての明確で単純な説明があります。

このセクションには、アレイが一意に参照されるようにする方法、アレイを強制コピーする方法、および2つのアレイがストレージを共有しているかどうかを確認する方法に関する情報も含まれています。


61
私はあなたがデザインの大きな赤い旗の共有解除とコピーの両方を持っているという事実を見つけます。
2014

9
これは正しいです。エンジニアは、言語設計ではこれは望ましくなく、Swiftの今後のアップデートで「修正」したいと考えていると私に説明しました。レーダーで投票します。
エリックカーバー2014

2
Linuxの子プロセスのメモリ管理におけるコピーオンライト(COW)のようなものですよね?おそらく、私たちはそれを長さ変更のコピー(COLA)と呼ぶことができます。これはポジティブなデザインだと思います。
2014年

3
@justhalf私は、SOに来る混乱した初心者の束を予測し、それらの配列が共有された/共有されなかった理由を尋ねることができます(あまり明確でない方法で)。
John Dvorak 14年

11
@justhalf:COWはとにかく現代の悲観化です。次に、COWは実装のみの手法であり、このCOLAは完全にランダムな共有と共有解除につながります。
子犬14年

25

Swift言語の公式ドキュメントから

添え字構文で単一の値を設定しても配列の長さを変更する可能性がないため、添え字構文で新しい値を設定する場合、配列はコピーされないことに注意してください。ただし、新しい項目を配列に追加する場合は、配列の長さを変更します。これにより、新しい値を追加した時点で配列の新しいコピーを作成するようにSwiftに求められます。以降、aは配列の独立した独立したコピーです。

このドキュメントのセクション「配列の割り当てとコピー動作」全体をお読みください。あなたはわかりますあなたが行うとき、配列内の項目の範囲を置き換え、その後、アレイは、すべての項目に自身のコピーを取ります。


4
ありがとう。私は質問の中で漠然とそのテキストを参照しました。しかし、下付き文字の範囲を変更しても長さは変更されず、それでもコピーされる例を示しました。したがって、コピーが必要ない場合は、一度に1つの要素を変更する必要があります。
クトゥツ2014

21

Xcode 6ベータ3で動作が変更されました。配列は参照型ではなくなり、コピーオンライトメカニズムを備えています。つまり、配列の内容をいずれかの変数から変更するとすぐに、配列はコピーされ、 1つのコピーが変更されます。


古い答え:

他の人が指摘したように、Swiftは、単一のインデックスの値を一度変更する場合を含め、可能であれば配列のコピー回避しようとします

配列変数(!)が一意であること、つまり別の変数と共有されていないことを確認したい場合は、unshareメソッドを呼び出すことができます。参照が1つだけでない限り、これは配列をコピーします。もちろんcopy、常にコピーを作成するメソッドを呼び出すこともできますが、他の変数が同じ配列を保持していないことを確認するには、共有解除をお勧めします。

var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a               // [1, 42, 3]
b               // [1, 2, 3]

うーん、私にとって、そのunshare()メソッドは未定義です。
Hlung 2014

1
@Hlungベータ3で削除されました。答えを更新しました。
Pascal

12

動作はArray.Resize.NET のメソッドに非常に似ています。何が起こっているのかを理解するには.、C、C ++、Java、C#、およびSwiftでトークンの履歴を確認すると役立つ場合があります。

Cでは、構造体は変数の集合体にすぎません。.構造型の変数にを適用すると、構造内に格納されている変数にアクセスします。オブジェクトへのポインターは変数の集約を保持しませんがそれらを識別します。構造体を識別するポインタがある場合、->演算子を使用して、ポインタで識別される構造体内に格納されている変数にアクセスできます。

C ++では、構造体とクラスは変数を集約するだけでなく、それらにコードを添付することもできます。を使用.してメソッドを呼び出すと、変数に対して、そのメソッドに変数自体の内容に作用するように要求します。使用した->オブジェクトがオブジェクトに作用するという方法を聞いてきます識別する変数で識別変数で。

Javaでは、すべてのカスタム変数タイプはオブジェクトを識別するだけであり、変数に対してメソッドを呼び出すと、変数によって識別されるオブジェクトがメソッドに通知されます。変数は、どのような種類の複合データ型も直接保持することはできません。また、メソッドが呼び出される変数にアクセスするための手段もありません。これらの制限は、意味的に制限がありますが、ランタイムを大幅に簡素化し、バイトコード検証を容易にします。このような簡素化により、市場がそのような問題に敏感であったときにJavaのリソースオーバーヘッドが削減され、市場での牽引力の獲得に役立ちました。また.、CまたはC ++で使用されているものと同等のトークンが必要ないことも意味していました。Javaは->CおよびC ++と同じ方法で使用できたかもしれませんが、作成者は単一文字を使用することを選択しました. 他の目的には必要なかったからです。

C#およびその他の.NET言語では、変数はオブジェクトを識別したり、複合データ型を直接保持したりできます。複合データ型の変数で使用すると、変数の内容.作用します。参照型の変数で使用すると、識別されたオブジェクトに作用します.それによります。一部の種類の操作では、意味の区別は特に重要ではありませんが、他の種類では重要です。最も問題のある状況は、呼び出された変数を変更する複合データ型のメソッドが読み取り専用変数で呼び出される状況です。読み取り専用の値または変数でメソッドを呼び出そうとすると、コンパイラーは通常、変数をコピーし、メソッドに作用させて変数を破棄します。これは通常、変数を読み取るだけのメソッドでは安全ですが、変数に書き込むメソッドでは安全ではありません。残念ながら、.doesには、そのような置換で安全に使用できるメソッドと使用できないメソッドを示す手段がまだありません。

Swiftでは、集約のメソッドは、それらが呼び出される変数を変更するかどうかを明示的に示すことができ、コンパイラーは、読み取り専用変数でのメソッドの変更の使用を禁止します(変数の一時コピーを変更するのではなく、破棄されます)。この違いがあるため、.トークンを使用して、それらが呼び出される変数を変更するメソッドを呼び出すことは、.NETよりもSwiftの方がはるかに安全です。残念ながら、.変数によって識別される外部オブジェクトに作用するために同じトークンがその目的で使用されるという事実は、混乱の可能性が残っていることを意味します。

タイムマシンがあり、C#やSwiftの作成に戻った場合、C ++の使用方法に非常に近い方法で言語.->トークンを使用することにより、このような問題に関する混乱の多くを遡及的に回避できます。集約と参照タイプの両方のメソッドは、それらが呼び出さ.れた変数->に作用し、(複合の場合)またはそれによって識別されたもの(参照タイプの場合)に作用するために使用できます。ただし、どちらの言語もそのように設計されていません。

C#では、メソッドが呼び出される変数を変更するメソッドの通常の方法は、変数をrefパラメーターとしてメソッドに渡すことです。したがって、20要素の配列を識別するArray.Resize(ref someArray, 23);ときにwhenを呼び出すと、元の配列に影響を与えることなく、23要素の新しい配列が識別されます。の使用は、メソッドが呼び出される変数を変更することを期待されるべきであることを明確にします。多くの場合、静的メソッドを使用せずに変数を変更できると便利です。構文を使用することを意味するSwiftアドレス。欠点は、どのメソッドが変数に作用するか、どのメソッドが値に作用するかについての明確化を失うことです。someArraysomeArrayref.


5

私にとっては、最初に定数を変数に置き換えると、より意味があります。

a[i] = 42            // (1)
e[i..j] = [4, 5]     // (2)

最初の行でのサイズを変更する必要はありませんa。特に、メモリ割り当てを行う必要はありません。の値に関係なくi、これは軽量な操作です。ボンネットの下がaポインターであると想像すると、定数ポインターになる可能性があります。

2行目はもっと複雑かもしれません。iおよびの値によっては、jメモリ管理が必要になる場合があります。これがe配列の内容を指すポインターであると想像すると、それが定数ポインターであるとはもう想定できません。新しいメモリブロックを割り当て、古いメモリブロックから新しいメモリブロックにデータをコピーし、ポインタを変更する必要がある場合があります。

言語デザイナーは(1)を可能な限り軽量に保つことを試みたようです。(2)はとにかくコピーを必要とする可能性があるので、コピーをしたかのように常に動作するというソリューションに頼っています。

これは複雑ですが、たとえば「if in(2)iとjがコンパイル時の定数であり、コンパイラーがeのサイズが正しくないことを推測できるなど)変更する場合は、コピーしません」


最後に、Swift言語の設計原則についての私の理解に基づいて、私は一般的なルールは次のとおりだと思います。

  • letデフォルトでは常に定数()をどこでも使用してください。大きな驚きはありません。
  • 変数(var)は、どうしても必要な場合にのみ使用し、そのような場合は注意して変更してください。

5

私が見つけたものは次のとおりです。操作が配列の長さを変更する可能性がある場合に限り、配列は参照される配列の変更可能なコピーになります。最後の例である、f[0..2]多数でのインデックス作成では、操作によって長さが変更される可能性があるため(重複が許可されていない可能性があります)、コピーされます。

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e // 4,5,3
f // 1,2,3


var e1 = [1, 2, 3]
var f1 = e1

e1[0] = 4
e1[1] = 5

e1 //  - 4,5,3
f1 // - 4,5,3

8
「長さが変わったように扱われる」長さが変更された場合にコピーされることは理解できますが、上記の引用と組み合わせると、これは本当に気になる「機能」であり、多くの人が誤解すると思う
ジョエルバーガー

25
言語が新しいからといって、明白な矛盾が含まれていても問題はありません。
Orbitでの軽さのレース

これはベータ3で「修正」され、var配列は完全に変更可能になり、let配列は完全に不変になりました。
Pascal

4

Delphiの文字列と配列は、まったく同じ「機能」を持っていました。実装を見たとき、それは理にかなっています。

各変数は動的メモリへのポインタです。そのメモリには、配列内のデータが後に続く参照カウントが含まれています。したがって、配列全体をコピーしたり、ポインタを変更したりせずに、配列の値を簡単に変更できます。配列のサイズを変更する場合は、より多くのメモリを割り当てる必要があります。その場合、現在の変数は新しく割り当てられたメモリを指します。ただし、元の配列を指す他のすべての変数を簡単に追跡することはできないため、そのままにしておきます。

もちろん、より一貫した実装を行うことは難しくありません。すべての変数でサイズ変更を確認する場合は、次のようにします。各変数は、動的メモリに格納されているコンテナへのポインタです。コンテナーは、参照カウントと実際の配列データへのポインターの2つを正確に保持します。配列データは、ダイナミックメモリの別のブロックに格納されます。これで、配列データへのポインターは1つだけになり、サイズを簡単に変更でき、すべての変数に変更が反映されます。


4

多くのSwiftアーリーアダプターは、このエラーが発生しやすい配列のセマンティクスについて不満を述べており、Chris Lattnerは、配列のセマンティクスが完全な値のセマンティクスを提供するように修正されたと書いています(アカウントを持つユーザー向けのApple Developerリンク)。これが正確に何を意味するかを確認するには、少なくとも次のベータ版を待つ必要があります。


1
新しいアレイの動作は、iOS 8 / Xcodeの6ベータ3に付属するSDKの通り利用可能になりました
smileyborg

0

これには.copy()を使用します。

    var a = [1, 2, 3]
    var b = a.copy()
     a[1] = 42 

1
コードを実行すると、「値の型 '[Int]'にはメンバー 'copy'がありません」と表示されます
jreft56

0

それ以降のSwiftバージョンで配列の動作に変更はありましたか?私はあなたの例を実行します:

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b

そして私の結果は[1、42、3]と[1、2、3]です

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.