🐈

【C#】参照渡し/値渡しと参照戻り値の概要と使用法

2021/12/04に公開

はじめに

公式ドキュメントで分かりにくいと感じた部分を端的にまとめた内容と独自の表現が含まれるのでまず公式ドキュメントを参照することをお勧めします。

前提

Func(y)
という関数の引数に
x
を渡して
z
が返り値
という状況で説明していきます。

値渡し

xとyはただ同じ値を持っているというだけで、格納している値自体はお互いに完全に無関係。
ただし、参照型においては同一の参照先を持つ場合に限って、xとyがインスタンスを介して同期していると勘違いしやすい。
(↑これがどうしても参照渡しっぽくみえてしまう。)

参照渡し

yはxの化身。yは常にxを監視してその動きを真似ている。ただし、yはxに変更を要求できる。

参照渡しは明示的に使う

参照渡しと値渡しは挙動がかなり異なります。
例えばFunc(y)が参照渡しのyに対して変更を行う場合、Func(y)はxに対して毎回異なるzを返すということになります。
一方、値渡しではyを変更してもxにその変更が伝播しないのでFunc(y)はxに対して毎回同じzを返します。
(ただし参照型では値渡しでも参照先のインスタンスは変更されるのでその限りではない)

値渡しと参照渡しの違いの図解

プログラミング言語というのは主に値型と参照型が存在しており、これらに対するを値渡しと参照渡しの違いを一口に説明するのは難しいです。特に、「参照型の値渡し」と「参照型の参照渡し」で混乱が生じてるようです。

また、後述の「参照戻り値」にてサンプルコードを掲載しているのでそちらを参考にするとより詳しく理解できると思います。

値型

値型.jpg
参照渡しでは当然、y=5という変更を行うとxは5になります。

参照型

インスタンスはヒープ領域に存在しており、参照型の変数はそのインスタンスへのアドレスが格納されています。
参照型.jpg
画像のようにxとyが同一の参照先をもっている場合、xとyはまるで参照型のように同期していると思いがちです。
ほとんどがこの状況なので、参照型には参照渡しか存在しないかのように感じるかもしれませんが、
参照型の変数というのはそもそもインスタンスの「アドレス」が格納されているにすぎません。「参照型の参照渡し」というのはこのアドレスに対しての話で、アドレス先のインスタンスについては関係ありません。
画像のαはインスタンスではなく参照先のアドレスです。
値型と同様に、参照型でも値渡しではyはxとは全く無関係にαを変更できますし、参照渡しではyはxの化身として全ての影響が伝播します。

C#で参照渡しするには?

refキーワード

メソッドの引数にrefキーワードをつけると「参照渡し」になります。

  • 引数に渡す値は初期化されている必要がある。
  • メソッドをコールするときも引数にrefキーワードをつける。
  • refキーワードの有無だけでオーバーロードできる。
  • ただし、後述のin,outも同様だが、ref,in,outだけではオーバーロードできない。

inキーワード

メソッドの引数にinキーワードをつけると「参照渡しの読み取り専用」になる。「入力参照引数」といいます。基本はrefキーワードと同じです。

  • メソッドをコールするときは通常通り呼べるが、inキーワードをあえてつけることも可能。というかつけるべき。
  • inキーワードはその引数自体には副作用が生じないことを保証している。

それは値渡しと一緒では?

表面的な挙動は大体同じですが、先に説明したとおり値渡しと参照渡しは内部では異なる動作をしています。
したがって、状況によっては入力参照引数にすることでパフォーマンスが向上する場合があります。

outキーワード

outキーワードをつけたものは「出力引数」と言います。

  • ref,inと異なり関数に渡すときに変数の初期化が不要。
  • その上で関数内で初期化する制約がある。

ところで、「参照渡し」では関数が参照元を変更できるので実質的な「複数の返り値」というものを実現できます。
outキーワードはその性質上この「複数の返り値」を実現するために用いられることがあります。
以上のことを踏まえてoutキーワードの使い道は

  • 関数によって変数を初期化したい。
  • 関数に複数の返り値をもたせたい。

の2つになります。

複数の返り値について

outキーワードは上記のように「複数の返り値」を安全に実現するための回避策だったようですが、C#7.0以降はTuple型で記述できるためその目的では使うことはもうないと思います。

参照戻り値

参照戻り値というのは、戻り値にrefキーワードをくっつけた格好になります。
戻り値が参照渡しになるので、「戻り値を受け取った側でその返り値の参照先を変更」できます。
つまり、Func(y)の返り値zの参照先を関数の外から後で変更できます。

通常通り値返しするサンプルコード

いつも通りです。

using System;

namespace ValueSample
{
    class GameWareManager
    {
        //field
        //property
        //function
        static void Main(string[] args)
        {
            GameWarehouse _gameWarehouse = new GameWarehouse();
            Game[] _myGames = new Game[]
            {
                new Game() { GameTitle = "UNDERTALE", ReleaseYear = 2015, },
                new Game() { GameTitle = "FF7", ReleaseYear = 1997, }
            };

            _gameWarehouse.StoreGames(_myGames);
            _gameWarehouse.DisplayGamelists();

            //_storedGamesを値渡し。
            var storedGames = _gameWarehouse.StoredGames;
            //storedGamesの参照先を変更(_storedGamesとは無関係。)
            storedGames = new Game[]
            {
                new Game() { GameTitle = "DELTARUNE", ReleaseYear = 2018, },
                new Game() { GameTitle = "FF7RE", ReleaseYear = 2020, },
            };
            //_storedGamesには関係ないので参照先が変わらない。
            _gameWarehouse.DisplayGamelists();
        }
    }
}
public class Game{
    //field
    //property
    public string GameTitle { get; set; } = "unknown";
    public int ReleaseYear { get; set; } = 0;
    //function
}

public class GameWarehouse{
    //field
    private Game[] _storedGames;
    //property
    public Game[] StoredGames{ 
        get
        {
            return _storedGames;
        }
        set
        {
            _storedGames = StoredGames;
        }
    }
    //function
    public GameWarehouse(){
       _storedGames = new Game[] { new Game() };
    }

    public void StoreGames(Game[] myGames){
        _storedGames = myGames;
    }
    public void DisplayGamelists(){
        foreach(var storedGame in _storedGames){
            Console.WriteLine(storedGame.GameTitle + ":" + storedGame.ReleaseYear);
        }
        Console.WriteLine();
    }
}

出力

UNDERTALE:2015
FF7:1997

UNDERTALE:2015
FF7:1997

参照返しするサンプルコード

refローカル変数の仕様に関しては公式ドキュメントを参照してください。
ちなみにですが、プロパティは内部的には関数と同じ扱いのようです。

using System;

namespace RefSample
{
    class GameWareManager
    {
        //field
        //property
        //function
        static void Main(string[] args)
        {
            GameWarehouse _gameWarehouse = new GameWarehouse();
            Game[] _myGames = new Game[]
            {
                new Game() { GameTitle = "UNDERTALE", ReleaseYear = 2015, },
		new Game() { GameTitle = "FF7", ReleaseYear = 1997, }
            };

            _gameWarehouse.StoreGames(_myGames);
            _gameWarehouse.DisplayGamelists();

            //_storedGamesを参照渡し。
            ref var storedGames = ref _gameWarehouse.StoredGames;
            //refローカル変数のstoredGamesの参照先を変更(_storedGamesの変更と同義)
            storedGames = new Game[]
            {
                new Game() { GameTitle = "DELTARUNE", ReleaseYear = 2018, },
                new Game() { GameTitle = "FF7RE", ReleaseYear = 2020, },
            };
            //_storedGamesの参照先が変わる。
            _gameWarehouse.DisplayGamelists();
        }
    }
}
public class Game{
    //field
    //property
    public string GameTitle { get; set; } = "unknown";
    public int ReleaseYear { get; set; } = 0;
    //function
}

public class GameWarehouse{
    //field
    private Game[] _storedGames;
    //property
    //propertyの返り値を参照渡しにする
    public ref Game[] StoredGames{ 
        get
        {
            return ref _storedGames;
        }
    }
    //function
    public GameWarehouse(){
       _storedGames = new Game[] { new Game() };
    }

    public void StoreGames(Game[] myGames){
        _storedGames = myGames;
    }
    public void DisplayGamelists(){
        foreach(var storedGame in _storedGames){
            Console.WriteLine(storedGame.GameTitle + ":" + storedGame.ReleaseYear);
        }
        Console.WriteLine();
    }
}

出力

UNDERTALE:2015
FF7:1997

DELTARUNE:2018
FF7RE:2020

参考

https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/in-parameter-modifier

Discussion