📑

C#:構造体におけるthisのこと

2023/03/30に公開

C#における構造体では、インスタンスメソッドで自身のデータを書き換えることができます。

using System;

var hoge = new Hoge();

Console.WriteLine(hoge.value); // 0

hoge.IncreaseValue();

Console.WriteLine(hoge.value); // 1

struct Hoge
{
    public int value;
    
    public void IncreaseValue()
    {
        this.value++;
    }
}

これはクラスと同様の挙動なので一見違和感がありませんが、構造体が値型であることを考えるとちょっと混乱する挙動です。例えば構造体を引数として渡す場合、データはコピーされるため、呼び出し側のデータは変更されません。

using System;

var hoge = new Hoge();

Console.WriteLine(hoge.value); // 0

Hoge.IncreaseValue(hoge);

Console.WriteLine(hoge.value); // 0 (変化しない!)

struct Hoge
{
    public int value;
    
    public static void IncreaseValue(Hoge target)
    {
        target.value++;
    }
}

種明かしをすると、構造体におけるインスタンスメソッドでは、thisref扱いとなっています。つまり、呼び出し側のデータへの参照(ポインタ)が暗黙的に渡されているということです。
上の引数Hoge targetrefにしてみると、最初の例と同様の結果が得られます。

using System;

var hoge = new Hoge();

Console.WriteLine(hoge.value); // 0

Hoge.IncreaseValue(ref hoge);

Console.WriteLine(hoge.value); // 1 (変化する)

struct Hoge
{
    public int value;
    
    public static void IncreaseValue(ref Hoge target)
    {
        target.value++;
    }
}

thisそのものを書き換える

thisref扱いなので、実はthis = new Hoge()みたいな書き方ができてしまいます。これはクラスではできないのでビビりますね。

これを悪用すると、インスタンスメソッドからreadonlyなフィールドを書き換えるみたいなことが可能なようです。

using System;

var hoge = new Hoge();

Console.WriteLine(hoge.value); // 0

hoge.IncreaseValue();

Console.WriteLine(hoge.value); // 1

struct Hoge
{
    public readonly int value;
    
    Hoge(int value)
    {
        this.value = value;
    }
    
    public void IncreaseValue()
    {
        //これはできない
        //this.value++; <- error CS0191: A readonly field cannot be assigned to
        
        //これはできちゃう
        this = new Hoge(this.value + 1);
    }
}

https://ufcpp.net/study/csharp/resource/readonlyness/#this-rewrite

非同期メソッドにおけるthis

構造体のインスタンスメソッドをasyncに指定すると、thisref扱いが解除されます。つまり、通常の引数と同様にコピーされます。

using System;

var hoge = new Hoge();

Console.WriteLine(hoge.value); // 0

hoge.IncreaseValue();

Console.WriteLine(hoge.value); // 0 (変化しない!)

struct Hoge
{
    public int value;
    
    public async void IncreaseValue()
    {
        this.value++;
    }
}

このように、通常のメソッドのつもりでフィールドを書き換えようとしても書き換わってくれません!これは気を抜いていると簡単にハマってしまう……。

しかし考えてみれば、そもそもC#では非同期メソッドがref引数を持てないのでした。非同期メソッドの処理は呼び出し元のスコープから外れるので、呼び出し元のスタック上にあるデータの寿命が保証できなくなってしまいます。原理的には当然の挙動ですが、いささか直感に反する!

おわり

ということで、このパターンは初めて出くわしたので記事にしてみました。構造体を扱うプログラムは動作原理をきちんと理解する必要がある場面が多いですね……。

Discussion