none
値型と参照型のデータのメソッド引数に渡したときの動きとstring型の動き RRS feed

  • 質問

  • こんにちは、基礎的なことなのですが、C#の値型と参照型のデータをメソッドに渡したときの動きについて質問です。
    以下のテストプログラムは、次のとおりに動いていると考えたのですが、正しいのでしょうか。気になって調べたのですが、相談できる人がおらず、答え合わせができないで困っています。
    お手数ですが、わかる方いましたらご意見を頂けると助かります。よろしくお願いします。

    c1の変数は、クラスなので参照型。
    参照型c1をメソッドに渡す。メソッドはデフォルトではコピーを作って値渡しをする。

    メソッドの中で、class1のフィールドの値を編集する。
    値型の値を[a=44, b=88]に書き変える。
    参照型class2の値[c]の値型の値を[a=400,b=200]に書き変える。
    これは、class1自体が参照型だから値の書き換えが有効になる。
    (class1をコピーしていても、参照してる先が一緒だから)

    新しいclass1を作って、引数に上書きする。
    でも、これはデフォルトのメソッドは値渡しだからcla1は変更されない。


    s1の変数は、構造体なので値型。
    値型s1をメソッドに渡す。メソッドは参照渡しなので、同じメモリのデータをメソッドに渡している。

    メソッド内で、struct1のフィールドの値を編集する。
    値型の値を[a=44,b=88]に書き変える。
    値型struct2の値[c]の値型の値を[a=400,b=200]に書き変える。
    これは、struct1自体が値型だから通常は書き変えされないけど、メソッドが参照渡しだからデータを直接が書き換える。

    新しいstruct1を作って、引数に上書きする。
    通常の値私だと書き換えされないけど、参照渡しだからval1の参照先自体が変更される。
    (最初に書き変えていたデータを捨てて、別のデータを参照し直す)


    st1とst2は参照型だから、文字列を格納する連続したメモリのデータを参照してる。

    例(メモリアドレス:100番地から始まってる)
    '0x41', '0x42', '0x043', '0x00' が、100から103番地にある

    st2にdefを追加すると通常は、st1も影響を受けてしまう
    st2はそうならないように、別のメモリアドレスに文字列を再作成してる?

    例(メモリアドレス:200番地にコピーを作成してst1とst2は別々のアドレスを参照する?)
    '0x41', '0x42', '0x043', '0x44', '0x45', '0x046', '0x00'

    テストのプログラム

            public void Run()
            {
                Class1 c1 = new Class1();
                c1.a = 10;
                c1.b = 12;
                ChangeTestClass(c1);
    
                Class1 c2 = null;
                ChangeTestClass(c2);
    
                Console.WriteLine("c1: a = {0}, b = {1}, c({2},{3})", c1.a, c1.b, c1.c.a, c1.c.b);
                //Console.WriteLine("c2: a = {0}, b = {1}, c({2},{3})", c2.a, c2.b, c2.c.a, c2.c.b); // c2はnull
    
                Struct1 s1 = new Struct1();
                s1.a = 10;
                s1.b = 12;
                ChangeTestStruct(ref s1);
                
                Console.WriteLine("s1: a = {0}, b = {1}, c({2},{3})", s1.a, s1.b, s1.c.a, s1.c.b);
    
                // string型の動き
                string st1 = "abc";
                string st2 = st1;
    
                st2 += "def";
                Console.WriteLine(st1);
                Console.WriteLine(st2);
            }
    
            /// <summary>
            /// 参照型の値渡しのテストです。
            /// </summary>
            /// <param name="cla1">cla1は参照型だけど、メソッドの引数は通常値渡し。</param>
            public void ChangeTestClass(Class1 cla1)
            {
                // メンバーの書き換え
                if (cla1 != null)
                {
                    cla1.a = 44;
                    cla1.b = 88;
                    cla1.c.a = 400;
                    cla1.c.b = 200;
                }
    
                var newRef = new Class1();
    
                newRef.a = 100;
                newRef.b = 200;
                newRef.c = new Class2 { a = 100, b = 100 };
    
                // 引数の書き換え
                cla1 = newRef;
            }
    
            /// <summary>
            /// 値型の参照渡しのテストです。
            /// </summary>
            /// <param name="val1">val1は値型で、メソッドの引数は参照渡し。</param>
            public void ChangeTestStruct(ref Struct1 val1)
            {
                // メンバーの書き換え
                //if (val1 != null)
                {
                    val1.a = 44;
                    val1.b = 88;
                    val1.c.a = 400;
                    val1.c.b = 200;
                }
    
                var newVal = new Struct1();
    
                newVal.a = 100;
                newVal.b = 200;
                newVal.c = new Struct2 { a = 100, b = 100 };
    
                // 引数の書き換え
                val1 = newVal;
            }

    テストプログラム
    https://onedrive.live.com/redir?resid=8CBEECC7554D8261!4215&authkey=!ALCOUcOICKLINSs&ithint=file%2czip

    参考
    http://dobon.net/vb/dotnet/beginner/valuereference.html
    https://msdn.microsoft.com/ja-jp/library/s6938f28.aspx
    http://ufcpp.net/study/csharp/oo_reference.html

    2016年3月17日 9:52

回答

  • クラスと構造体の参照渡し・値渡しは合ってます。

    文字列は操作すると新しく作成されます。
    C#はポインタが使えるので確認してみるといいです。

    private static unsafe void Test()//プロジェクトのプロパティでunsafeを許可すること
    {
        string st1 = "abc";
        string st2 = st1;
        fixed (char* p1 = st1, p2 = st2)
        {
            Console.WriteLine(new IntPtr(p1).ToString("X"));
            Console.WriteLine(new IntPtr(p2).ToString("X"));
        }
        st2 += "def";
        fixed (char* p2 = st2)
        {
            Console.WriteLine(new IntPtr(p2).ToString("X"));
        }
    }


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答としてマーク ichiethel 2016年3月18日 0:53
    2016年3月17日 12:01

すべての返信

  • クラスと構造体の参照渡し・値渡しは合ってます。

    文字列は操作すると新しく作成されます。
    C#はポインタが使えるので確認してみるといいです。

    private static unsafe void Test()//プロジェクトのプロパティでunsafeを許可すること
    {
        string st1 = "abc";
        string st2 = st1;
        fixed (char* p1 = st1, p2 = st2)
        {
            Console.WriteLine(new IntPtr(p1).ToString("X"));
            Console.WriteLine(new IntPtr(p2).ToString("X"));
        }
        st2 += "def";
        fixed (char* p2 = st2)
        {
            Console.WriteLine(new IntPtr(p2).ToString("X"));
        }
    }


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答としてマーク ichiethel 2016年3月18日 0:53
    2016年3月17日 12:01
  • gekkaさん回答ありがとうございます。

    上記コードを実行してみました。

    今回は

    2133968
    2133968
    2135EFC

    という結果が帰ってきました。
    実行ごとに値は変わりますが、1、2行目の値が同じで3行目は異なる値になるところに違いはありませんでした。

    やはり新しく文字列を生成しているのですね。参照型だけど特別な動きをしているのですね。
    ポインターで直接アドレスを確認するというのは思いつきませんでした。

    とても分かりやすくて助かりました。ありがとうございます。

    2016年3月18日 0:53
  • string型について、以下のスレッドの後半部分を読まれると理解が深まるのではないかと思います。

    2つのList 内のオブジェクトをリンクさせる方法を教えてください
    https://social.msdn.microsoft.com/Forums/ja-JP/96df3709-fbef-41dd-94ab-0bed14476030/list-?forum=csharpgeneralja


    ★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/

    2016年3月18日 1:06
    モデレータ
  • すでに解決済みなので今さらながらのレスですが・・・

    > ポインターで直接アドレスを確認するというのは思いつきませんでした。

    st1 と st2 が同じオブジェクトを指しているかどうか(アドレスが同じかどうか)は、ポインタを調べなくても、object 型にキャストとして == 演算子で比較すれば分かります。

    具体的には以下の通りです。

    string st1 = "abc";
    string st2 = st1;
    
    Console.WriteLine((object)st1 == (object)st2);
    
    st2 += "def";
    Console.WriteLine(st1);
    Console.WriteLine(st2);
    Console.WriteLine((object)st1 == (object)st2);
    
    // 結果は:
    //  True
    //  abc
    //  abcdef
    //  False

    詳しくは以下の記事を参考にしてください。

    == 演算子 (C# リファレンス)
    https://msdn.microsoft.com/ja-jp/library/53k8ybth.aspx

    Equals() と演算子 == のオーバーロードに関するガイドライン
     (C# プログラミング ガイド)
    https://msdn.microsoft.com/ja-jp/library/ms173147(v=vs.90).aspx

    string 型は変更不可(変更すると新しいオブジェクトが作られる)ということ以外に、前者の記事サンプルコードのコメントにあるように "string interning points to same reference" という点にも要注意です。


    2016年3月18日 1:43
  • st1 と st2 が同じオブジェクトを指しているかどうか(アドレスが同じかどうか)は、ポインタを調べなくても、object 型にキャストとして == 演算子で比較すれば分かります。

    ちなみにオブジェクトが同一かの確認には、object.ReferenceEqualsというメソッドを使う方法もあります。

    個人的にはこちらの方が意図が明確になるのでお勧めです。

    ※もちろんキャストして==でも問題ありません。

    2016年3月18日 1:51
  • "string interning points to same reference" という点にも要注意です。

    インターンプールですね。以下をご紹介しておきます。

    (参考)
    パフォーマンスを気にするなら、String.Empty より "" と書いた方が良い。
    http://blogs.wankuma.com/shuujin/archive/2008/04/21/134497.aspx


    ★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/

    2016年3月18日 2:15
    モデレータ
  • SurferOnWwwさん、なちゃさん、trapemiyaさん回答ありがとうございます。

    下記のとおり、単純にどうなるか見てみました。
    いろいろ使ってないものがあるのがわかって助かりました。また、インターンプールについても整理できていないなぁと感じました。

            public void Test()
            {
                string st1 = "abc";
                string st2 = st1;
    
                fixed (char* p1 = st1, p2 = st2)
                {
                    Console.WriteLine(new IntPtr(p1).ToString("X"));
                    Console.WriteLine(new IntPtr(p2).ToString("X")); 
                }
    
                Console.WriteLine("st1 and st2 :{0}.", object.ReferenceEquals(st1, st2));
                st2 += "def";
                Console.WriteLine("st1 and st2 :{0}.", object.ReferenceEquals(st1, st2));
    
                fixed (char* p2 = st2)
                {
                    Console.WriteLine(new IntPtr(p2).ToString("X"));
                }
    
                string str1 = String.Empty;
                string str2 = String.Intern(String.Empty);
                Console.WriteLine(@"str1 and str2 == : {0}", ((object)str1) == ((object)str2));
            }

    また、以下の内容も読んでいたのですが、string.Emptyと""のReferenceEqualの結果が私の環境ではtrueになっています。(.Net2の結果とは異なっています)

    下記の画像のとおり、string.Emptyはフォールドで""はそのまま書かれているので、リンク先と同じ結果でした。
    注釈にある実行時に最適化されてしまうからなのでしょうか。(DebugとReleaseで同じでした)

    そもそも、パフォーマンス的にはどっちでも良いくらいの相当細かな話なんだと思います。
    ただ、仕様としてILの結果は違うメモリを参照しそうに見えるけど、一緒なんだな、という所が少し不思議に思いました。
    (string.Emptyを'aaa'なんかにするとFalseになります)

    >パフォーマンスを気にするなら、String.Empty より "" と書いた方が良い。 
    http://blogs.wankuma.com/shuujin/archive/2008/04/21/134497.aspx

    http://blogs.wankuma.com/shuujin/archive/2007/10/11/101557.aspx(古いけどこの内容と少し異なる)

    2016年3月18日 3:21
  • ついでのレスでなんですが、値型のボックス化とボックス化解除についても理解しておいた方がいいと思います。

    ボックス化とボックス化解除 (C# プログラミング ガイド)
    https://msdn.microsoft.com/ja-jp/library/yz2be5wk.aspx

    自分で int i = 123; object o = i; のようなコードを書くことはないかもしれませんが 、値型を object 型に代入したり、Object 型から特定の値型にキャストすることはよくあると思います。そのとき何が起こっているか理解しておくと今後の役に立つのではないでしょうか。


    • 編集済み SurferOnWww 2016年3月18日 12:53 脱字追加
    2016年3月18日 10:21
  • SurferOnWwwさん回答ありがとうございます。

    正直、ボックス化とボックス化解除は、こうした意識がありませんでした。

    以下のコードの部分は今回の質問の内容と近い部分で、特に参考になりました。

     

    int i = 123;

    object o = i;

    i = 456;

    System.Console.WriteLine("The value-type value = {0}", i); // 456

    System.Console.WriteLine("The object-type value = {0}", o); // 123

     

    値型intと参照型objectとでは、数値の管理方法がメモリ的に異なっている。

    スタックとヒープの2つがあって、値型はスタックに参照型はヒープの(メモリ)領域で管理する。

     

    上記の場合、値型iはスタックで管理していた値(123)を参照型oに渡すと、ボックス化することになった値(123)はヒープ上にコピーされます。

    コピーされた値を参照型oが持つことになるので、スタック上の値型iの値(123)を456に変更しても参照型o(123)はヒープ上のコピーされた値なので影響を受けない。

    もとの説明:

    この例は、元の値型とボックス化されたオブジェクトが別個のメモリ位置を使用するため、それぞれ別々の値を格納できることを示しています。

     

    とても参考になりました。

    2016年3月23日 5:10