C#

【C#】値型と参照型

2020年12月30日

内部での変数の扱いの違い

似て非なるコード

まず最初に次の2つのプログラムを見てください。

using System;

class Program {
    static void Main(string[] args) {
        Counter c = new Counter(0);
        Console.WriteLine($"before={c.Count}\n");
        PlusOne(c);
        Console.WriteLine($"after={c.Count}\n");
    }
    static void PlusOne(Counter c) { c.Count += 1; }
}
struct Counter {
    public int Count { get; set; }
    public Counter(int initialValue) {
        Count = initialValue;
    }
}

(cAlgoではなくC#のコンソールアプリケーションです。VisualStudio→新規プロジェクト→C#コンソールアプリケーションで適当な名前で新規作成して、このままコピペで動きます)

Counter型のオブジェクトを作って、Counter型を引数にとるメソッドで中身を+1して、最後に結果を表示させているだけの単純なプログラムです。

このコードの結果はこうなります。

before = 0
after = 0

おや、メソッドで+1されたはずなのに0のままですね。なぜでしょうか。

では次をご覧ください。

using System;

class Program {
    static void Main(string[] args) {
        Counter c = new Counter(0);
        Console.WriteLine($"before={c.Count}\n");
        PlusOne(c);
        Console.WriteLine($"after={c.Count}\n");
    }
    static void PlusOne(Counter c) { c.Count += 1; }
}
class Counter {
    public int Count { get; set; }
    public Counter(int initialValue) {
        Count = initialValue;
    }
}

ほぼ同じコードですが、Counterが構造体(struct)ではなくクラスになっています。ぱっと見、同じ動きをしそうに見えるかもしれませんが、結果はこうなります。

before = 0
after  = 1

ちゃんとメソッド内で+1されたものが反映されています。これは何が違うのでしょうか。

 

値型と参照型

この違いが値型と参照型の違いなんです。

 

説明の前に

オブジェクト指向では変数=オブジェクトと考えることが多いですが、この記事では「変数」と「オブジェクト」は概念的に別の意味を持つと思って読んでください。

ここでは「変数」と言ったらCounter cなどと宣言された c を指します。「オブジェクト」と言ったら、どっかのメモリ上に存在する c が表すモノ(値)本体を指します。

この定義であれば「値型」と「参照型」は変数の型の種類ということになります。そして、変数をメソッドの引数に渡すということは一般的に「変数の中身をそのままコピーしてメソッドに渡す」ということ示します。

重要なのでもう一度。引数に渡すということは、その変数の型が値型であろうと参照型であろうと「変数の中身をそのままコピーしてメソッドに渡す」ということなのです。

これを念頭に置いたうえでこの先をどうぞ。

 

構造体Counter型の変数は値型

C#では構造体は値型になります。

値型の変数はオブジェクト本体を自分で持ちます。つまり上記変数c の中身をのぞくとCounter型構造体オブジェクトそのものが入ってるイメージです。オブジェクトを使うときは中身のオブジェクトを直接使います。

引数に渡すときは「変数の中身をそのままコピーして渡す」ので、このCounter型オブジェクト本体をコピーしてメソッドに渡してあげてます。メソッドが使うのはび出し元が用意したオブジェクトのコピー品なのです。

上記一つ目のプログラムではメソッド内で+1されたCounter型オブジェクトはMain内で宣言して最後に内容を表示させたCounter型オブジェクトとは全くの別物なので、メソッド内で+1されようとなにしようと、最後の表示には何ら影響を与えません。

Main内の変数cが指すオブジェクトのプロパティは初期値0で作られて表示されただけなので、当然結果は0のままです。

 

クラスCounter型の変数は参照型

C#ではクラスは参照型になります。

参照型の変数はオブジェクト本体はどっか別の場所に置いておき、その場所情報だけを自分で持ちます。つまり変数cの中身をのぞくと、Counter型クラスオブジェクトが入ってると思いきや、そこにはオブジェクトがある場所だけがメモしてあるだけなんです。オブジェクトを使うときはその場所を「参照」して、その先にあるオブジェクトを使います。

引数に渡すときは「変数の中身をそのままコピーして渡す」ので、このオブジェクト場所のメモをコピーして渡してあげるのです。メソッドはその場所情報を「参照」して、そこにあるオブジェクト、つまり結果的に呼び出しが用意したものと全く同じオブジェクトを使います

であれば、上記2つ目のプログラムではMainでの変数cが指すオブジェクトのプロパティは、初期値0で作られてから、PlusOneメソッド内で+1されて、表示されたということになるため、結果は1になるのです。

 

C#では型の種類により自動的に決まる

C#ではint,doubleなどの組み込みの単純な値と構造体、列挙型は値型文字列(string)やその他クラスはすべて参照型と決まっています。

プログラマが、参照型にするか値型にするかというのを考える必要はありません。その代わり自分の使ってる型がどっちなのかを意識し、挙動を把握したうえでコーディングしていくことが重要となります。

 

 

参照渡し」別の話

「参照渡し」は引数の渡し方を示す言葉

上記は型の種類の話でしたが、似た用語で、メソッドへの引数のを渡し方を表す言葉として「参照渡し」「値渡し」があります。

勘違いしないでいただきたいのですが、「参照型変数を引数に渡すのが参照渡し」ではありません!他の言語でそのような使い方をする人も一部いるようなのでややこしいのですが、少なくともC#では違う意味で使いますので注意してください。

 

C#でいう参照渡し

最初に「メソッドの引数に渡すというのは『変数の中身をそのままコピーしてメソッドに渡す』ということ」と言いました。これがC#を含む多くのプログラミング言語で一般的な引数の渡され方で、引数の「値渡し」と呼ばれます。

これに対し、C#では「参照渡し」という特別な引数の渡し方ができます。「変数自身の参照(場所情報だけ)を渡す」ということができるんです。メソッドの定義時にパラメータにrefキーワードやout,inキーワードをつけることで参照渡しとすることができます。

渡される変数自身が値型か参照型かは関係ありません。何を渡されようとも、その変数の参照で渡すのが参照渡しですので、値型を渡されたら値型の参照を渡しますし、参照型を渡されれば参照型の参照で渡します。

using System;

class Program {
    static void Main(string[] args) {
        int n = 0;
        Console.WriteLine($"before={n}\n");
        // ↓呼び出し時も参照渡しであることを示すrefが必要
        EditParam(ref n); 
        Console.WriteLine($"after={n}\n");
    }
    //-------------------------------------------
    // 引数を参照渡しでもらって編集するメソッド
    static void EditParam(ref int param) { //← ref をつけると参照渡しになる
        // ここでのparamはMainのnを別の名前で持ち込んでると考えていい。
        // こんな風に代入してもnの値も連動して変わる。
        param = 100; 
    }
}

結果はこうなります。

before = 0
after = 100

なにが違うかわかりにくければ、refキーワードを消して動作の違いを確かめてみてください。

参照型の参照とか言われてもわかりにくければ、「参照渡しでは他で宣言された変数をそのまま持ち込むことができる」と考えるとわかりやすいかもしれません。

 

値型の「参照渡し」=参照型の「値渡し」?

違います!実行してみると全く違うのがわかります。

値型の参照渡し

これは値型を参照渡しするコードですが、一番わかりやすい違いは13行目、引数に直接代入処理をしたときの挙動です。

using System;

class Program {
    static void Main(string[] args) {
        Counter c = new Counter(0);
        Console.WriteLine($"before={c.Count}\n");
        EditParam(ref c); 
        Console.WriteLine($"after={c.Count}\n");
    }
    //-------------------------------------------
    // 引数を参照渡しでもらって編集するメソッド
    static void EditParam(ref Counter param) { 
        param = new Counter(100); 
    }
}
struct Counter {
    public int Count { get; set; }
    public Counter(int initialValue) {
        Count = initialValue;
    }
}

結果はこうなります。まぁ実質的に前項と同じコードですからね。

before = 0
after = 100

代入処理をしてもMain側の変数まで連動して変わるというのがポイントです。

 

参照型の値渡し

上のコードの2か所のrefキーワードを消して、structをclassに置き変えるだけで、参照型の「値渡し」になります。

そうすると、結果は当然こうなります。

before = 0
after = 0

むしろ引数に直接別のものを代入した時の挙動としては、こっちのが自然に感じる方が多いのではないでしょうか。

 

挙動の違い

引数に「参照渡し」で渡された場合は、参照型値型にかかわらず、変数自体が共有されます。引数本体が「参照の参照」となるため、引数=変数として扱えるのです。引数に別のものを代入すれば、呼び出し元の変数内のオブジェクトも連動して変わり、引き続き共有されます。

対して、参照型変数が引数に「値渡し」で渡された場合は、引数中身の参照が指し示すオブジェクトのみが共有されます。引数本体はただの「参照のコピー」ですので、引数自体を書き換える代入では、引数側は変わりますが、オブジェクトの共有は切れ、呼び出し元の変数にはなにも影響ありません

値型の参照渡しと参照型の値渡し、字面だけ見ると同じように見えてしまいますが、別物ですので間違いないようにしましょう。

 

「参照渡し」は何に使うのか

どういうときに使うのか、一つ実例を示します。

文字列を数値に変換するメソッドを作りたいと思ったとします。しかし、この処理は失敗することもあり、成功したかどうかを返り値で受け取りたい、しかし変換した結果の数値もなんらかの形で受け取りたいという状況を考えてください。

さてC#ではメソッドの返り値は一つしか受け取れません。どうしましょうか。いくつか手段はありますが、ここでは引数で結果を受け取る方向で考えます。このときに結果を入れるための引数を「参照渡し」で渡しておき、結果を入れてもらうような設計にするのです。

例えばこんな感じ。最後の引数はoutキーワードを付けることで「参照渡し」で受け取るようにしています。(refキーワードでも内部的には同じですが、返り値代わりのように必ずメソッド内で代入する場合はoutキーワードの方がおすすめです。)

bool TryParse(string s, out int result);

あ、もしかしてこんなメソッド見たことありますか?ですよね、もともとある組み込みのメソッドですからね。

参考までにintやdoubleの静的メソッドTryParseはこんな風に使えます。

class Program {
    static void Main(string[] args) {
        string str = Console.ReadLine();
        int n;
        bool res = int.TryParse(str, out n);
        if (res) {
            Console.WriteLine($"入力は整数{n}です。");
        } else {
            Console.WriteLine("入力は整数ではありません。");
        }
    }
}

 参照渡しする変数nは初期化の必要はありませんし、なんなら渡すときに宣言してもかまいません。(outキーワードなら。refキーワードだとこれができません。)

この「参照渡し」自体は自分から積極的に使う必要はないのですが、こんな感じで使われてるメソッドもあるため、使い方くらいは知っておいてもいいかもしれません。

 

 

まとめ

値型と参照型の挙動の違いはしっかり理解しておいた方がいいです。「参照渡し」は仕組みとして一応知っておきましょう。

値型  → 型の種類。値型変数を他で使うときはコピーを渡すので基本的に他のメソッドと共有はできない

参照型 → 型の種類。参照型変数の指すオブジェクト本体は他のメソッドと共有可能

値渡し  → 変数を引数に渡す普通の渡し方。特に意識しなくていい。

参照渡し → 引数に渡すときの特別な渡し方。メソッド内に他で宣言された変数をそのまま持ち込める
       渡される変数が値型か参照型かは関係ない。

あと、繰り返しになりますが単に参照型変数を引数に渡すことを「参照渡し」と呼ぶ人もいるみたいなので、その辺は空気読んでうまく対応しましょう。

 

-C#

© 2024 cTrader's Life Powered by AFFINGER5