破棄 - C# の基礎

破棄は、アプリケーション コードで意図的に使用されないプレースホルダー変数です。 破棄は、未割り当ての変数と同等です。つまり、値がありません。 破棄によって、あなたのコードを読み取るコンパイラおよびその他のユーザーに対して、次のような意図が伝わります。あなたは式の結果を無視するつもりでした。 あなたは、式の結果、タプル式の 1 つ以上のメンバー、メソッドの out パラメーター、またはパターン マッチング式のターゲットを無視したい可能性があります。

破棄することで、コードの意図が明確になります。 破棄するとは、コードでその変数が使用されないことを意味します。 そのため、読みやすさと保守容易性が向上します。

変数を破棄と指定するには、変数名にアンダースコア (_) を指定します。 たとえば、次のメソッド呼び出しでタプルが返され、1 つ目と 2 つ目の値は破棄です。 area は、GetCityInformation によって返される 3 つ目のコンポーネントに設定された、以前に宣言された変数です。

(_, _, area) = city.GetCityInformation(cityName);

C# 9.0 以降では、破棄を使用して、ラムダ式の未使用の入力パラメーターを指定できます。 詳細については、ラムダ式に関する記事の「ラムダ式の入力パラメーター」セクションを参照してください。

_ が有効な破棄の場合、その値を取得しようとすると、または代入演算で使用しようとすると、"名前 '_' は、現在のコンテキストに存在していません" というコンパイラ エラー CS0301 が生成されます。 このエラーの原因は、_ に値が割り当てられておらず、記憶域の場所も割り当てることができないことです。 実際の変数であれば、前の例のように、複数の値を破棄できません。

タプルとオブジェクトの分解

分解は、複数のタプルがあり、アプリケーション コードで一部のタプル要素を使用し、その他の要素を無視する場合に便利です。 たとえば、次の QueryCityDataForYears メソッドは、市区町村名、その地域、年、市区町村のその年の人口、2 つ目の年、市区町村のその 2 つ目の年の人口というタプルを返します。 この例は、2 つの年の間に変化した人口数を示しています。 タプルから使用できるデータのうち、市区町村の地域は使用しません。また、指定時に市区町村名と 2 つの日付はわかっています。 そのため、タプルに格納されている 2 つの人口値のみが必要であり、残りの値は破棄対象として処理できます。

var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");

static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
{
    int population1 = 0, population2 = 0;
    double area = 0;

    if (name == "New York City")
    {
        area = 468.48;
        if (year1 == 1960)
        {
            population1 = 7781984;
        }
        if (year2 == 2010)
        {
            population2 = 8175133;
        }
        return (name, area, year1, population1, year2, population2);
    }

    return ("", 0, 0, 0, 0, 0);
}
// The example displays the following output:
//      Population change, 1960 to 2010: 393,149

破棄を使用したタプルの分解の詳細については、「タプルとその他の型の分解」を参照してください。

また、クラス、構造体、またはインターフェイスの Deconstruct メソッドを使用すると、オブジェクトから特定セットのデータを取得し、分解することもできます。 分解された値の一部のみが必要な場合は、破棄を使用できます。 Person オブジェクトを 4 つの文字列 (名、姓、市区町村、州) に分解し、姓と州を破棄する例を次に示します。

using System;

namespace Discards
{
    public class Person
    {
        public string FirstName { get; set; }
        public string MiddleName { get; set; }
        public string LastName { get; set; }
        public string City { get; set; }
        public string State { get; set; }

        public Person(string fname, string mname, string lname,
                      string cityName, string stateName)
        {
            FirstName = fname;
            MiddleName = mname;
            LastName = lname;
            City = cityName;
            State = stateName;
        }

        // Return the first and last name.
        public void Deconstruct(out string fname, out string lname)
        {
            fname = FirstName;
            lname = LastName;
        }

        public void Deconstruct(out string fname, out string mname, out string lname)
        {
            fname = FirstName;
            mname = MiddleName;
            lname = LastName;
        }

        public void Deconstruct(out string fname, out string lname,
                                out string city, out string state)
        {
            fname = FirstName;
            lname = LastName;
            city = City;
            state = State;
        }
    }
    class Example
    {
        public static void Main()
        {
            var p = new Person("John", "Quincy", "Adams", "Boston", "MA");

            // Deconstruct the person object.
            var (fName, _, city, _) = p;
            Console.WriteLine($"Hello {fName} of {city}!");
            // The example displays the following output:
            //      Hello John of Boston!
        }
    }
}

破棄を使用したユーザー定義型の分解の詳細については、「タプルとその他の型の分解」を参照してください。

switch を使用したパターン マッチング

"破棄パターン" は、switch 式を使用したパターン マッチングで使用できます。 null も含め、あらゆる式は常に破棄パターンと一致します。

switch 式を使用して、オブジェクトが IFormatProvider 実装を提供しているかどうかを判断し、オブジェクトが null かどうかをテストする ProvidesFormatInfo メソッドの定義例を次に示します。 また、破棄パターンを使用して、その他の任意の型の null 以外のオブジェクトを処理します。

object?[] objects = { CultureInfo.CurrentCulture,
                   CultureInfo.CurrentCulture.DateTimeFormat,
                   CultureInfo.CurrentCulture.NumberFormat,
                   new ArgumentException(), null };
foreach (var obj in objects)
    ProvidesFormatInfo(obj);

static void ProvidesFormatInfo(object? obj) =>
    Console.WriteLine(obj switch
    {
        IFormatProvider fmt => $"{fmt.GetType()} object",
        null => "A null object reference: Its use could result in a NullReferenceException",
        _ => "Some object type without format information"
    });
// The example displays the following output:
//    System.Globalization.CultureInfo object
//    System.Globalization.DateTimeFormatInfo object
//    System.Globalization.NumberFormatInfo object
//    Some object type without format information
//    A null object reference: Its use could result in a NullReferenceException

out パラメーターを使用したメソッドの呼び出し

Deconstruct メソッドを呼び出してユーザー定義型 (クラス、構造体、またはインターフェイスのインスタンス) を分解する場合、個々の out 引数の値を破棄できます。 また、out パラメーターを指定して任意のメソッドを呼び出すときに、out 引数の値を破棄することもできます

次の例では、DateTime.TryParse(String, out DateTime) メソッドを呼び出して、現在のカルチャで日付の文字列表現が有効かどうかを判断します。 この例では、日付文字列の検証のみが目的で、解析して日付を抽出する処理は行わないため、メソッドの out 引数は破棄されます。

string[] dateStrings = {"05/01/2018 14:57:32.8", "2018-05-01 14:57:32.8",
                      "2018-05-01T14:57:32.8375298-04:00", "5/01/2018",
                      "5/01/2018 14:57:32.80 -07:00",
                      "1 May 2018 2:57:32.8 PM", "16-05-2018 1:00:32 PM",
                      "Fri, 15 May 2018 20:10:57 GMT" };
foreach (string dateString in dateStrings)
{
    if (DateTime.TryParse(dateString, out _))
        Console.WriteLine($"'{dateString}': valid");
    else
        Console.WriteLine($"'{dateString}': invalid");
}
// The example displays output like the following:
//       '05/01/2018 14:57:32.8': valid
//       '2018-05-01 14:57:32.8': valid
//       '2018-05-01T14:57:32.8375298-04:00': valid
//       '5/01/2018': valid
//       '5/01/2018 14:57:32.80 -07:00': valid
//       '1 May 2018 2:57:32.8 PM': valid
//       '16-05-2018 1:00:32 PM': invalid
//       'Fri, 15 May 2018 20:10:57 GMT': invalid

スタンドアロンの破棄

スタンドアロンの破棄を使用して、無視対象として任意の変数を指定できます。 一般的な用途の 1 つは、割り当てを使用して、引数が null でないことを確認することです。 次のコードでは、破棄を使用して割り当てを強制しています。 代入の右側には null 合体演算子が使用され、引数が null の場合に System.ArgumentNullException がスローされます。 このコードに割り当ての結果は不要なため、破棄されます。 式によって null チェックが強制的に実行されます。 破棄を使用して、割り当ての結果は必要ではないか、使用されない、という意図を明確にします。

public static void Method(string arg)
{
    _ = arg ?? throw new ArgumentNullException(paramName: nameof(arg), message: "arg can't be null");

    // Do work with arg.
}

次の例では、スタンドアロンの破棄を使用して、非同期操作で返される Task オブジェクトを無視します。 タスクの割り当ての結果、この処理が完了するときにスローされる例外が抑制される効果があります。 これにより、次のようなあなたの意図が明確になります。あなたは、Task を破棄し、その非同期操作から生成されるエラーを無視したいと考えています。

private static async Task ExecuteAsyncMethods()
{
    Console.WriteLine("About to launch a task...");
    _ = Task.Run(() =>
    {
        var iterations = 0;
        for (int ctr = 0; ctr < int.MaxValue; ctr++)
            iterations++;
        Console.WriteLine("Completed looping operation...");
        throw new InvalidOperationException();
    });
    await Task.Delay(5000);
    Console.WriteLine("Exiting after 5 second delay");
}
// The example displays output like the following:
//       About to launch a task...
//       Completed looping operation...
//       Exiting after 5 second delay

タスクを破棄に割り当てないと、次のコードにより、コンパイラの警告が生成されます。

private static async Task ExecuteAsyncMethods()
{
    Console.WriteLine("About to launch a task...");
    // CS4014: Because this call is not awaited, execution of the current method continues before the call is completed.
    // Consider applying the 'await' operator to the result of the call.
    Task.Run(() =>
    {
        var iterations = 0;
        for (int ctr = 0; ctr < int.MaxValue; ctr++)
            iterations++;
        Console.WriteLine("Completed looping operation...");
        throw new InvalidOperationException();
    });
    await Task.Delay(5000);
    Console.WriteLine("Exiting after 5 second delay");

注意

デバッガーを使用して上の 2 つのサンプルのいずれかを実行すると、例外がスローされたときにデバッガーによってプログラムが停止します。 デバッガーがアタッチされていない場合、どちらの場合も例外は警告なしで無視されます。

_ は有効な識別子でもあります。 サポートされるコンテキスト以外で _ を使用すると、破棄対象ではなく、有効な変数として扱われます。 _ という識別子が既にスコープ内にある場合、スタンドアロンの破棄として _ を使用すると、次のような結果になります。

  • 意図した破棄の値を割り当てることで、スコープ内の _ 変数の値が誤って変更される。 次に例を示します。
    private static void ShowValue(int _)
    {
       byte[] arr = { 0, 0, 1, 2 };
       _ = BitConverter.ToInt32(arr, 0);
       Console.WriteLine(_);
    }
     // The example displays the following output:
     //       33619968
    
  • タイプ セーフに違反するコンパイラ エラーが発生する。 次に例を示します。
    private static bool RoundTrips(int _)
    {
       string value = _.ToString();
       int newValue = 0;
       _ = Int32.TryParse(value, out newValue);
       return _ == newValue;
    }
    // The example displays the following compiler error:
    //      error CS0029: Cannot implicitly convert type 'bool' to 'int'
    
  • コンパイラ エラー CS0136 "ローカルまたはパラメーター '_' は、その名前が外側のローカルのスコープでローカルやパラメーターの定義に使用されているため、このスコープでは宣言できません" が発生する。次に例を示します。
     public void DoSomething(int _)
    {
     var _ = GetValue(); // Error: cannot declare local _ when one is already in scope
    }
    // The example displays the following compiler error:
    // error CS0136:
    //       A local or parameter named '_' cannot be declared in this scope
    //       because that name is used in an enclosing local scope
    //       to define a local or parameter
    

関連項目