5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】Classの参照を渡すと内部の変数が参照渡しと同様の振る舞いをする件について

5
Last updated at Posted at 2024-10-22

つたないですがTestCodeを
とても意外な結果でした。
指摘があったのですが、これを参照値渡しと呼び、参照渡しとは区別されるそうです。
まあ普通に便利だったんでいいんですが。

参照値渡しの実験的Code

XAML(実行用)

<Window x:Class="DataClassの参照渡し.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DataClassの参照渡し"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <StackPanel>
        <Button x:Name="button1" Height="50" Width="100"
        HorizontalAlignment="Left" Content="button1" Click="button1_Click"/>
            <Button x:Name="button2" Height="50" Width="100"
HorizontalAlignment="Left" Content="button2" Click="button2_Click"/>
            <Label x:Name="label1" FontSize="16"/>
                
        </StackPanel>
    </Grid>
</Window>

Code Behaind

using System.Windows;

namespace DataClassの参照渡し
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

        }
        DataClassA dataA = null!;
        private void button1_Click(object sender, RoutedEventArgs e)
        {
            //Buttonを押すとisSucessffmpegがtrue

            dataA = new();
            dataA.isSucessffmpeg = true;
            label1.Content = $"{nameof(dataA.isSucessffmpeg)} は $\"{dataA.isSucessffmpeg}\"";
    //結果
    //sSucessffmpeg ( $"True"
        }

        private void button2_Click(object sender, RoutedEventArgs e)
        {
            //Buttonを押すとDataClassAを参照渡しし、dataClassBのisSucessffmpegがfalse
            DataClassB dataB = new(dataA);
            dataB.dataA.isSucessffmpeg = false;

            label1.Content = $"{nameof(dataB.isSucessffmpeg)} は $\"{dataA.isSucessffmpeg}\"";
          //結果  isSucessffmpeg は $"False"
        }


        public class DataClassA
        {


            public bool isSucessffmpeg;
        }
        public class DataClassB
        {
            public DataClassB(DataClassA dataA)
            {

                dataB = dataA;
                //データクラスを参照渡し
            }



          public  DataClassB dataB;
        }

    }
}

普通こうなると思わないじゃないですか?
Falseにならなかったらstaticで渡すつもりだったんで想定外の挙動でした(笑)

ChatGPTが同じことを言うので、ハルシネーションだと思っていました。

更に言うと、普通こんなことしないでメソッドで引数取ると思う(笑) 別に完全コンストラクタじゃないし

ChatGPTによると参照型の基本的な仕様だそうで、C#1.0から変わってないそうです。あんま気づきにくい事だと思う:sweat:

これは参照渡し(ref)ではなく参照値渡しである

という指摘があった。

そこでChatGPTで何が出来て、何が出来ないのか聞いてみた。

参照渡し(ref)でできて、参照値渡し(通常の参照型オブジェクトの渡し方)ではできないことをいくつか具体的に説明します。

1. 呼び出し元の変数自体を変更する

参照渡し(ref)では、呼び出し元の変数そのものを関数やメソッド内で変更することができます。これに対し、参照値渡し(通常の参照型オブジェクトの渡し方)では、変数の中身(オブジェクト自体)のプロパティなどを変更することはできますが、変数そのものを別のオブジェクトに置き換えることはできません

例: 参照渡し (ref) の場合

public class DataClass
{
    public bool isSuccess;
}

public void ModifyObject(ref DataClassA dataA)
{
    // 新しいオブジェクトを生成して、元のオブジェクトを置き換える
    dataA = new DataClass();
    dataA.isSuccess = true;
}

DataClassA dataA = new DataClass();
ModifyObject(ref dataA);
// myObjは新しいオブジェクトに置き換えられ、isSuccessはtrueに設定される

この例では、refを使っているため、ModifyObjectメソッド内でmyObj変数自体を新しいインスタンスに置き換えることができます。呼び出し元のmyObjは、新しいオブジェクトを指すようになります。

例: 通常の参照値渡しの場合

public void ModifyObject(DataClass obj)
{
    // これは元のオブジェクトに影響を与えない
    obj = new DataClass();
    obj.isSuccess = true;
}

DataClass myObj = new DataClass();
ModifyObject(myObj);
// myObjは元のオブジェクトのままで、isSuccessも変更されない

この場合、objに新しいインスタンスを代入しても、呼び出し元のmyObjは変更されませんobjはローカルな変数なので、関数のスコープが終わるとobjへの変更は消えます。

2. 値型でもオブジェクトのプロパティを変更できる

参照渡し(ref)は、値型(structなど) を渡す場合にも使えます。この場合、関数内で値型のフィールドやプロパティを変更しても、呼び出し元に反映されます。一方、通常の値渡しでは、値型のコピーが渡されるため、関数内での変更は呼び出し元に影響を与えません。

例: 値型の参照渡し

public struct MyStruct
{
    public int Value;
}

public void ModifyStruct(ref MyStruct myStruct)
{
    myStruct.Value = 100;
}

MyStruct myStruct = new MyStruct();
ModifyStruct(ref myStruct);
// 呼び出し元の myStruct.Value が 100 に変更される

refを使うことで、値型であっても元のオブジェクトの値を変更できます。

例: 通常の値渡し

public void ModifyStruct(MyStruct myStruct)
{
    myStruct.Value = 100;
}

MyStruct myStruct = new MyStruct();
ModifyStruct(myStruct);
// myStruct.Value は変更されない

この場合、myStructはコピーされて渡されるため、関数内での変更は呼び出し元に影響を与えません。

3. 複数のoutref引数を使って値を返す

refoutキーワードを使うと、メソッドが複数の値を返すことが可能です。通常の参照値渡しでは、1つのオブジェクトのプロパティなどを変更することはできますが、複数の変数に対して一度に値を返すことはできません。

例: 複数のref引数

public void ModifyValues(ref int a, ref int b)
{
    a += 10;
    b += 20;
}

int x = 5;
int y = 15;
ModifyValues(ref x, ref y);
// x は 15, y は 35 に変更される

この場合、2つのref引数を使って、2つの異なる変数の値を一度に変更することができます。

まとめ

  • 呼び出し元の変数自体を変更できる: refを使うと、呼び出し元の変数自体を新しいオブジェクトで置き換えることが可能。通常の参照値渡しでは、このような動作はできません。
  • 値型に対しても影響を与えられる: refを使えば、値型(structなど)でも呼び出し元に変更を反映させることが可能。
  • 複数の変数に対して一度に値を返す: refoutを使えば、複数の変数に同時に値を返すことができるが、通常の参照値渡しではこれは不可能です。

これらの特徴により、refを使うことで、通常の参照値渡しではできない強力な操作が可能になります。

あとがき

通常の参照値渡しでも十分だと思うけど(今回の場合はアドレスのコピーで十分)、まあrefの方がいろいろ出来るのは分った。

何時もストックいいねありがとうございますひつじ。

なぜ上記のようにクラスを値渡しするだけで内部のメンバーも伝播するのかという件について

DataClassAとかの方が分かりやすいと思た(笑)

もちろんrefとは異なる。

Classは参照型なので、参照型を値渡しする → 参照値渡し

ここまでは理解できると思う。

この場合、


 DataClassA dataA = null!;
 public button2DataClass(DataClaasA dataA)
            {

                dataB = dataA;
                //データクラスを参照渡し
            }

この時点で 「同じインスタンス」を参照している

DataClassB という「別名」から同じオブジェクトの isProcess を true にしているわけです。

だから結果的に DataClassA.isProcess も true になります。

struct(構造体)なら「値型」なので別々にコピーされ、A を変えても B には影響しない(らしい AI談)。

refとの違い(参照値渡しを使わない場合)

public class DataClassA
{
    public bool Flag;
}
DataClassB dataB;
// 通常の参照値渡し
void Normal(DataClassA dataA)
{
    obj = new DataClassB(); // 呼び出し元には影響しない
    dataA.Flag = true;       // 同じオブジェクトなら反映される
    

    Debug.Writeiine(dataB.Flag.toString());
      //Classの値渡しをしなければFalseのまま
}

refの場合


public class DataClassA
{
    public bool Flag;
}

public class DataClassB
{
    public bool Flag;
}

// ref
void WithRef(ref DataClassA dataA)
{

   dataA = new DataClassB(); // 呼び出し元の変数自体が置き換わる
    
    dataA.Flag = true;
    
    Debug.Writeline(dataB.Flag.toString());
    //trueになる
}


//Outは意味合いが違うので割愛
呼び出し
var a = new DataClass();
a.Flag = false;

Normal(a);     // a.Flag は falseのまま(オブジェクト置換は影響なし)
WithRef(ref a); // a は新しいインスタンスを指し、Flag は true

ポイントまとめ

通常の参照値渡し:オブジェクトの中身は変更できるが、変数そのもの(参照先)を置き換えることはできない。 →本記事はこの事を利用している

ref:呼び出し元の変数自体を上書き可能。既存のインスタンスを更新したり置き換えたりできる。 →柔軟に運用でき、Code量を削減できる

最後に

今回の方法は ref / out を使わずに呼び出し元のプロパティを変更できる という小ネタです。

ただし、この実装はイミュータブルではなく、副作用として呼び出し先でプロパティが意図せず書き換わってしまう可能性があります。

実用性は限定的であり、むしろアンチパターンとなるリスクもあるため、簡単なケースにとどめるのが無難です。

5
4
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?