zukucode
主にWEB関連の情報を技術メモとして発信しています。

C# 一時的に変えた設定をusingで元に戻す

一時的に表示言語などの設定を変え、処理が終わったら元に戻したい場合があります。

変更と復元を離れた場所へ書くと、途中で例外が発生したり、returnが追加されたりしたときに、復元処理へ到達しなくなることがあります。

今回は、一時変更と後片付けを1つのクラスへまとめ、usingで範囲を明示する方法を紹介します。例では、リソースの言語選択に使われるCultureInfo.CurrentUICultureを変更します。

後で戻すだけでは例外に対応できない

復元へ到達しない可能性がある例
var original = CultureInfo.CurrentUICulture;
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");

// ここで処理する。例外が発生すると次の行へ進まない。

CultureInfo.CurrentUICulture = original;

復元を確実に行う基本は、tryとfinallyを使う方法です。

try-finallyで元に戻す例
var original = CultureInfo.CurrentUICulture;
try
{
    CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
    // 言語を切り替えた状態で処理する
}
finally
{
    CultureInfo.CurrentUICulture = original;
}

この処理が1か所だけなら、これで十分です。同じ後片付けを複数の場所へ書くようになったら、小さなクラスへまとめる方法があります。

変更と復元を同じクラスへ置く

UiCultureScope.cs
using System;
using System.Globalization;

public sealed class UiCultureScope : IDisposable
{
    private readonly CultureInfo original;
    private bool disposed;

    public UiCultureScope(CultureInfo culture)
    {
        ArgumentNullException.ThrowIfNull(culture);
        original = CultureInfo.CurrentUICulture;
        CultureInfo.CurrentUICulture = culture;
    }

    public void Dispose()
    {
        if (disposed) return;
        CultureInfo.CurrentUICulture = original;
        disposed = true;
    }
}

コンストラクターで元の設定を保存して変更し、Disposeで元に戻します。IDisposableは、この後片付けの処理を持つことを示すinterfaceです。

disposedは同じインスタンスで二重に復元しないための変数です。このクラスは作成した処理の範囲で使い、別の処理へ渡して後から破棄する使い方はしません。

usingで適用する範囲を囲む

Program.cs
using System;
using System.Globalization;

var before = CultureInfo.CurrentUICulture;

using (var scope = new UiCultureScope(CultureInfo.GetCultureInfo("en-US")))
{
    Console.WriteLine(CultureInfo.CurrentUICulture.Name); // en-US
}

Console.WriteLine(CultureInfo.CurrentUICulture.Equals(before)); // True

.NET 8以降のコンソールアプリで使える例です。

usingのブロックを抜けるとDisposeが呼ばれます。通常の終了だけでなく、ブロック内の例外やreturnで抜ける場合も後片付けされます。Microsoft Learnのusingの説明でも確認できます。

ファイルを閉じる処理だけでなく、このように「一定の範囲だけ変更し、終わったら戻す」という使い方もできます。

using varでは戻るタイミングが変わる

using宣言の例
using var scope = new UiCultureScope(CultureInfo.GetCultureInfo("en-US"));
// この宣言を含むスコープの終わりまで言語変更が続く

using varは短く書けますが、次の行で元に戻るわけではありません。宣言を含むスコープの終わりで破棄されます。

メソッドの途中だけ言語を変えたいなら、ブロックで囲む方が範囲を読み取りやすくなります。保守するときは「どこで変えたか」だけでなく、「どこまで変わったままか」を見えるようにします。

UI言語と数値の書式を混同しない

CurrentUICultureは、主に言語別リソースを選ぶための設定です。数値や日時の書式には、別のCurrentCultureが使われます。

この例のクラスはCurrentUICultureだけを変更します。日付を英語の形式にしたいからといって、このクラスだけで解決するとは限りません。

書式を変更したいだけなら、ToStringなどへCultureInfoを明示して渡せるかを先に確認します。設定を変更しない方法の方が、関数の入出力を追いやすい場合があります。

入れ子と例外を確認する

入れ子にした場合は、内側を抜けると外側の設定へ戻り、外側を抜けると最初の設定へ戻る必要があります。

確認する状況期待する結果
通常どおりブロックを抜ける元の言語へ戻る
ブロック内で例外が発生する呼び出し元で例外を受けた時点で元に戻っている
内側で別の言語へ変更する内側の終了で外側の言語へ戻る
同じインスタンスを2回破棄するこの例では2回目は何もしない

入れ子は、内側から順番に終了する前提です。スコープをフィールドへ保存したり、終了順序を入れ替えたりすると、期待する言語へ戻らない可能性があります。

また、この仕組みは並列処理を制御するロックではありません。別タスクの動作までまとめて管理する用途にはせず、バックグラウンド処理へ渡す言語は明示する方が追いやすくなります。

一時変更が不要なら引数を優先する

自分で作る関数なら、CreateMessage(language)のように言語を引数へ渡す方法もあります。

既存のライブラリが現在のUI言語を読むために一時変更が必要なら、変更と復元をまとめたスコープが役立ちます。使う範囲を短くし、元へ戻すコードを呼び出し側へ散らさないことが、この構成の狙いです。


関連記事