🔥

値渡し / 参照渡し と 値型/参照型 をごっちゃにしてしまう君に告ぐ 不変な参照型 は 値型と見分けがつかないぞ

に公開
20

値渡しと参照渡しの動作確認を 値型と参照型を例にしつつ 参照戻り値対応している配列で試してみる

C# では 配列のインデクサが参照戻り値対応しているのでそれでいってみます。

値型(ミュータブル)

例えばこういった構造体(値型)があったとするじゃないですか。

record struct V1(string Value);

C# でいう record struct は可変な 構造体レコードです。

次の様な操作をするとどうなるでしょうか?

V1[] array = [new("1"), new("2"), new("3"), new("4"), new("5")];
Console.WriteLine($"1:before: {ToString(array)}");
int i = 0;
{
    // 0: 配列のインデクサは参照戻り値
    array[i++].Value = "🐁";
}
{
    // 代入(値渡し)
    {
        var v = array[i++];
        //1: プロパティを変更
        v.Value = "🐄";
    }
    {
        var v = array[i++];
        //2: インスタンスを更新
        v = v with {
            Value = "🐅",
        };
    }
}
{
    // ref ローカル変数 (参照渡し)
    {
        ref var v = ref array[i++];
        //3: プロパティを変更
        v.Value = "🐈";
    }
    {
        ref var v = ref array[i++];
        //4: インスタンスを更新
        v = v with {
            Value = "🐒",
        };
    }
}
Console.WriteLine($"1:after : {ToString(array)}");
string ToString<T>(T[] array) => string.Join(",", array.Select(v => $"{v}"));
1:before: V1 { Value = 1 },V1 { Value = 2 },V1 { Value = 3 },V1 { Value = 4 },V1 { Value = 5 }
1:after : V1 { Value = 🐁 },V1 { Value = 2 },V1 { Value = 3 },V1 { Value = 🐈 },V1 { Value = 🐒 }

Index でいうところの
0: 反映される(インデクサから直接プロパティ変更)
1: 反映されない(値渡し+プロパティ変更)
2: 反映されない(値渡し+インスタンス変更)
3: 反映される(参照渡し+プロパティ変更)
4: 反映される(参照渡し+インスタンス変更)
となります。

参照型(可変)

例えばこういったクラス(参照型)があったとするじゃないですか。

record class V2(string Value) {
    public string Value {get;set;} = Value;
}

(※record class はデフォルトで get; init; で読み取り専用なのでわざわざ書き換えられる様にしている)

次の様な操作をするとどうなるでしょうか?


V2[] array = [new("1"), new("2"), new("3"), new("4"), new("5")];
Console.WriteLine($"2:before: {ToString(array)}");
int i = 0;
{
    //0: 配列のインデクサは参照戻り値
    array[i++].Value = "🐁";
}
{
    // 代入(値渡し)
    {
        var v = array[i++];
        //1: プロパティを変更
        v.Value = "🐄";
    }
    {
        var v = array[i++];
        //2: インスタンスを更新
        v = v with {
            Value = "🐅",
        };
    }
}
{
    // ref ローカル変数 (参照渡し)
    {
        ref var v = ref array[i++];
        //3: プロパティを変更
        v.Value = "🐈";
    }
    {
        ref var v = ref array[i++];
        //4: インスタンスを更新
        v = v with {
            Value = "🐒",
        };
    }
}
Console.WriteLine($"2:after : {ToString(array)}");
string ToString<T>(T[] array) => string.Join(",", array.Select(v => $"{v}"));
2:before: V2 { Value = 1 },V2 { Value = 2 },V2 { Value = 3 },V2 { Value = 4 },V2 { Value = 5 }
2:after : V2 { Value = 🐁 },V2 { Value = 🐄 },V2 { Value = 3 },V2 { Value = 🐈 },V2 { Value = 🐒 }

Index でいうところの
0: 反映される(インデクサから直接プロパティ変更)
1: 反映される(値渡し+プロパティ変更)
2: 反映されない(値渡し+インスタンス変更)
3: 反映される(参照渡し+プロパティ変更)
4: 反映される(参照渡し+インスタンス変更)
となります。

参照型(不変)

例えばこういったクラス(参照型)があったとするじゃないですか。

record class V3(string Value);

C# でいう record class は不変な クラスレコードです。

次の様な操作をするとどうなるでしょうか?

V3[] array = [new("1"), new("2"), new("3"), new("4"), new("5")];
Console.WriteLine($"3:before: {ToString(array)}");
int i = 0;
{
    //0: [直接編集不可] 配列のインデクサは参照戻り値
    i++;
    // array[i++].Value = "🐁";
}
{
    // 代入(値渡し)
    {
        var v = array[i++];
        //1: [直接編集不可]プロパティを変更
        // v.Value = "🐄";
    }
    {
        var v = array[i++];
        //2: インスタンスを更新
        v = v with {
            Value = "🐅",
        };
    }
}
{
    // ref ローカル変数 (参照渡し)
    {
        ref var v = ref array[i++];
        //3: [直接編集不可] プロパティを変更
        // v.Value = "🐈";
    }
    {
        ref var v = ref array[i++];
        //4: インスタンスを更新
        v = v with {
            Value = "🐒",
        };
    }
}
Console.WriteLine($"3:after : {ToString(array)}");
string ToString<T>(T[] array) => string.Join(",", array.Select(v => $"{v}"));
3:before: V3 { Value = 1 },V3 { Value = 2 },V3 { Value = 3 },V3 { Value = 4 },V3 { Value = 5 }
3:after : V3 { Value = 1 },V3 { Value = 2 },V3 { Value = 3 },V3 { Value = 4 },V3 { Value = 🐒 }

Index でいうところの
0: 許されない(インデクサから直接プロパティ変更)
1: 許されない(値渡し+プロパティ変更)
2: 反映されない(値渡し+インスタンス変更)
3: 許されない(参照渡し+プロパティ変更)
4: 反映される(参照渡し+インスタンス変更)
となります。

まとめ

つまり表にまとめるとこう

項目 値型+可変 参照型+可変 参照型+不変 値型+不変
インデクサから直接プロパティ変更 - -
値渡し+プロパティ変更 - -
値渡し+インスタンス変更
参照渡し+プロパティ変更 - -
参照渡し+インスタンス変更

上記の動作から見て、 値型の プロパティの変更も インスタンスの変更も同様の挙動とみて良さそうです。
また、インデクサからの直接変更は 参照渡し による変更に間違いは無さそうです。

参照型+不変とすることで 参照型の特性である インスタンスを変更せずにプロパティの変更ができなくなり、値型の挙動と同じになることが確認できました。

値渡ししかない言語であればまず違いはわからないですね。

以上。

使ったソース

sharplab

※ V4 として 読み取り専用の値型を追加しています

Discussion

dameyodamedamedameyodamedame

ある程度正確な分類をしたいのであれば、参考までに…
https://en.wikipedia.org/wiki/Evaluation_strategy
書きっぷりからも分かるとおり、さほど統一された用語でもありません。
個人的には回りくどい言い方は必ずしも必要ではないと思っています。

junerjuner

評価戦略は よく読むのでわかりみがふかい。

dameyodamedamedameyodamedame

返信が付いてたんですね。今気付きました。

記事を書いた意図が以下のどちらかよく分からなかったので、

A) 回りくどい表現を使うのは嫌いだし、こんなケースでないと違いがないと言いたい
B) 回りくどい表現が嫌ではなく、現実で違うケースを示したかった

A)だろうなと思ってさらに細かいことを言うコメントしたのですが、反応が鈍いのでB)なのかもしれませんね。副作用を嫌って最近明示的にレコード型を使うことも多いわけですが、javaのDTOやメソッドチェインなどで中身を変えないのは昔からのお作法ですよね。他方明示的に代入をする以上、classインスタンスだと参照先が変わってしまいます。このようなケースをあえてこういうケース分けに出してこざるを得ないほど、現実には参照渡しと参照の値渡しを区別したい状況がないということなんですよ。だってclassインスタンスだって誰もが知っているのですから…。

私は何も両者が同じだと言っているのではありません。初心者に説明するときなど、文脈によっては最初は両者を区別せずに説明していいだろうと言っているだけなのです。私がこの手の話を見たのは、参照型と値型の区別もできない右も左も分からない初心者に、経験者(私ではありません)が参照渡しを例に挙げて説明している状況でした。C/C++かJavaだったと思います。初心者がなるほどね、と納得しそうになったところで、参照渡し警察が颯爽と現れ、それは値渡しなので間違いだ嘘を教えるなと言って去っていくわけです。初心者にとってはいい迷惑ですよね…。それだけの話なのです。

両者を区別できるとこんなにいいことがある!とか、両者が区別できないとこういうときにすごい困るよ!みたいな話なら、口を酸っぱくして誰しも教えるわけですが、経験上そういうケースを見たことがないので、こういう話はただのウンチクだよなって思うってわけです。まあどっちか結局分からないので、参照渡し警察さん用の説明を入れといただけで他意はありません…。

junerjuner

値渡ししかできない環境だと 参照型であろうと 値型であろうと区別はできないよの意ですね。
よく JavaScript で プリミティブが 値型説明されてしまうので。(これ自体は仕様上保証できず、多分 JavaScript に於ける 文字列型とかは実装的には参照型であろうと言われます。

dameyodamedamedameyodamedame

値渡ししかできない環境だと 参照型であろうと 値型であろうと区別はできないよ

何と何を区別できないと言いたいのでしょう?冗談を言ってるのではなく?

JavaScript で プリミティブが 値型説明されてしまう

個人的には値型でいいと思いますが…。どういう不都合・懸念点・違和感があるのでしょうか?

junerjuner

語弊がありました。 参照型+不変 と 値型 は区別は つかないの意ですね。

個人的には値型でいいと思いますが…。どういう不都合・懸念点・違和感があるのでしょうか?

仕様としては とくにそこは明言していない為(値型と説明してしまうと実装依存になってしまう為)ですね。

javascript に於いては 不変であることとプロパティを持たない がプリミティブの仕様なので。(それ以外はオブジェクト

そこに値型云々は言うべきではないと思っています。(究極全て参照型でも辻褄は合うというスタンス

dameyodamedamedameyodamedame

語弊がありました。 参照型+不変 と 値型 は区別は つかないの意ですね。

表にまとめた部分のどこを指して区別がつかないと言っているのでしょうか?
条件も正確に指定してください。

仕様としては とくにそこは明言していない為(値型と説明してしまうと実装依存になってしまう為)ですね。

仕様として明言はしていませんが、値型と説明しても実装依存にはなりません。
(現在)ECMAScript 2024では

  1. Section 6.1.1〜6にPrimitive Valueの定義がある
  2. Section 6.2.5.5〜6にGetValue/PutValueの定義があり変数に対して値を操作している
  3. Section 13.15.2に代入操作の定義があり右辺の評価結果がプリミティブ値であれば、そのプリミティブ値そのものがコピーされて左辺の変数に関連付けられる
  4. Section 10.2.1に引数渡しの定義があり引数リストが評価され値が仮引数に渡される
  5. Section 7.2.14〜15に比較演算子の定義があり同じ型の場合プリミティブは値が同じかどうかで比較される

ので、無理に参照型と考える必要がありません。不都合も懸念点も違和感もなく、ただ信念として定義がないのならどちらかに決めないとしたいだけですよね?
説明の際にどちらかに決めたとして特に問題はないわけで、普通に選べば値型なだけです。

dameyodamedamedameyodamedame

会話になってないし、ChatGPTさんに聞いた話を載せられてもだから何としか思いません…

junerjuner

js に於ける プリミティブは 不変な メソッドを持たない 値であり、 その実装が 値型/参照型 であることは論点にしない方がいいのでは感はある。

多分特別扱いされている型くらいのニュアンスで見ておくべき感。

junerjuner

三値論でいうところの 真(TRUE)/偽(FALSE)/不明(NULL) のうち 不明 であるなら 不明 と教えるべき派ですね。私は。

dameyodamedamedameyodamedame

仕様なので、特定のエンジン実装を想定する必要はないですよね。stringがC/C++の構造を持たない型に分類されるはずがないので(ちなみにSpiderMonkyでメモリ上の配置の説明は200行までにはなく
https://github.com/mozilla/gecko-dev/blob/67e239d26b479dc64bf29b3f09a427a838b9585f/js/src/vm/StringType.h#L289-L392
あたりだと思います)。ようは仕様の説明としてstringを値型に分類するか参照型に分類するかだけの話なのですよ。

V8の方は見ていませんが、どちらもJITコンパイルしてネイティブでも動くようなコードなので、ちゃんと読み込まない限り、メモリ配置くらいしか確定はしないのでは?と思います。言いたいことを明確にしてから必要なときだけ持ち出すようにした方が賢明だと思いますよ。

三値論でいうところの 真(TRUE)/偽(FALSE)/不明(NULL) のうち 不明 であるなら 不明 と教えるべき派ですね。私は。

なのであれば、

不都合も懸念点も違和感もなく、ただ信念として定義がないのならどちらかに決めないとしたいだけですよね?

に対しては、Yesだということですね。ようやく話が進んで良かったです。

語弊がありました。 参照型+不変 と 値型 は区別は つかないの意ですね。

表にまとめた部分のどこを指して区別がつかないと言っているのでしょうか?
条件も正確に指定してください。

これについては未だ回答がありません。

正直会話が噛み合わずに明後日の方向に進むだけなら、各質問に対して「回答いつまで待ってくれ」などの時間指定が欲しいです。私も暇ではないので無限に会話することはできません。

junerjuner

表に於いては 参照型+不変 に構文が無い ものは - となっているだけなのでそれを除いて比較すれば同じではないでしょうか?
(そう言ってしまうと 参照渡しがあったとしても構文上は違いは無いになりますが。

dameyodamedamedameyodamedame

語弊がありました。 参照型+不変 と 値型 は区別は つかないの意ですね。

表にまとめた部分のどこを指して区別がつかないと言っているのでしょうか?
条件も正確に指定してください。

これについては未だ回答がありません。

表に於いては 参照型+不変 に構文が無い ものは - となっているだけなのでそれを除いて比較すれば同じではないでしょうか?
(そう言ってしまうと 参照渡しがあったとしても構文上は違いは無いになりますが。

「どこを指して」という意味ではどこなのでしょうか?
「条件も正確に指定してください」と補足までしてわざわざ聞いているので、正確にお願いします。

junerjuner

こういうニュアンスのつもりで言っていたのですが。

項目 値型+可変 参照型+不変
値渡し+インスタンス変更
参照渡し+インスタンス変更
dameyodamedamedameyodamedame

語弊がありました。 参照型+不変 と 値型 は区別は つかないの意ですね。

表にまとめた部分のどこを指して区別がつかないと言っているのでしょうか?
条件も正確に指定してください。

これについては未だ回答がありません。

表に於いては 参照型+不変 に構文が無い ものは - となっているだけなのでそれを除いて比較すれば同じではないでしょうか?
(そう言ってしまうと 参照渡しがあったとしても構文上は違いは無いになりますが。

「どこを指して」という意味ではどこなのでしょうか?
「条件も正確に指定してください」と補足までしてわざわざ聞いているので、正確にお願いします。

こういうニュアンスのつもりで言っていたのですが。

項目 値型+可変 参照型+不変
値渡し+インスタンス変更
参照渡し+インスタンス変更

インスタンス変更と言っているのは、with expressionを使って生成したインスタンスの代入のことですよね?
そもそも値型+可変でそれを使う意味があるのでしょうか?
原則不変でだけ意味を持つ構文ではないでしょうか?

junerjuner

たしかに!
不変+値型パターンを playground に追加したのであとでコード追加とタイトル変更します。

dameyodamedamedameyodamedame

何が確かになのか分からないし、何をどう変更したのか明確でない変更はやめてください。
変更するならいつ変更するのか、変更したならしたと明快に通知してください。
見る気がしません。

dameyodamedamedameyodamedame

一応追記しておくと、現状のあなたの主張をまとめると、インスタンス変更は不変でしか意味がないのだから、

項目 参照型+不変
値渡し+インスタンス変更
参照渡し+インスタンス変更

となり、この表の結果を元に

参照型+不変 と 値型 は区別は つかない

と言っているということになります。まるで筋が通っていません。意図が正しく伝わるように正確に記載してください。