🗂

C#: out引数がdiscard(破棄)されているかどうか判定する実験

2022/01/03に公開

はじめにお断りとして、本記事は実用性を狙ったものではありません。

ネタ

C#7.0以降、_ によるdiscard(破棄)が使えるようになりました。その用例の1つがout引数です。

bool isInt = int.TryParse("12345", out _);

本記事は、このように out _ として捨てられてしまうのか、または out var x のようにちゃんと受け取ってくれたのかを、呼び出されたメソッド内で判定できるのか?というのをやってみるものです。

先に結論

以下はC# 10.0 (.NET 6) 前提です。

void Foo(out int v, [CallerArgumentExpression("v")] string expression = "")
{
    v = default;
    if (expression != "_")
    {
        System.Threading.Thread.Sleep(1000); // 重い計算
	v = 12345;
    }
}

この例のように、discardされないときのみoutのため頑張って計算する、のようなシーンがたまにあるかもという仮定で考えてみました。

後述しますが限界があります。

繰り返しますが実用すべきものではないと考えます。これに近いことをやりたければdiscard版を別メソッドかオーバーロードで用意するのが良いでしょう。

CallerArgumentExpression について

C# 10.0から使えるようになった CallerArgumentExpression という属性があります。
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/caller-argument-expression

どのような式を渡したのかを文字列として得ることができます。

using System;
using System.Runtime.CompilerServices;
					
Test("Hello World");
Test("Hello" + "World");
Test(() => "Hello World");
Test(new[]{"Hello World"}[0]);

void Test(object obj, [CallerArgumentExpression("obj")] string expression = "")
{
    Console.WriteLine(expression);
}
"Hello World"
"Hello" + "World"
() => "Hello World"
new[]{"Hello World"}[0]

discard + CallerArgumentExpression

CallerArgumentExpressionを使えば、discardすなわち _ があるかどうか文字列としてわかります。本実験的にはありがたいことに、out キーワードは文字列に含まれません。

int x;
Foo(out x);
Foo(out int y);
Foo(out _);

void Foo(out int v, [CallerArgumentExpression("v")] string expression = "")
{
    v = default;
    Console.WriteLine(expression);
}
x
int y
_

ということで、再掲になりますが一応はこれで希望のことはできそうすね。

void Foo(out int v, [CallerArgumentExpression("v")] string expression = "")
{
    v = default;
    if (expression != "_")
    {
        System.Threading.Thread.Sleep(1000); // 重い計算
	v = 12345;
    }
}

引数が複数ある場合

CallerArgumentExpressionは複数個の引数があっても大丈夫です。

Foo2(out _, out _);
Foo2(out int x, out int y);

void Foo2(
    out int a, out int b, 
    [CallerArgumentExpression("a")] string expressionA = "",
    [CallerArgumentExpression("b")] string expressionB = "")
{
	a = default;
	b = default;
	Console.WriteLine("a: {0}", expressionA);
	Console.WriteLine("b: {1}", expressionB);
}
a: _
b: _
a: int x
b: int y

条件分岐がめんどくさそうですが、従って2個以上outがあっても同様にできますね。

制限事項

_ が使われたからと言ってdiscardとは限りません。

int _;
Foo(out _);
Console.WriteLine(_); // 捨ててないから12345を期待するが、0

void Foo(out int v, [CallerArgumentExpression("v")] string expression = "")
{
    v = default;
    if (expression != "_")
    {
	v = 12345;
    }
}

以上で実験は終わりです。簡単にできる範囲では、ほかに良い手は思い浮かびませんでした。思考のメモとして残します。

Discussion