ツナ缶雑記

ぐうたらSEのブログです。主にマイクロソフト系技術を中心に扱います。

名前空間宣言の内側に using ディレクティブを置くと Using の削除と並び替えが動作しない

f:id:masatsuna:20220219155026p:plain

暗黙的な using ディレクティブと global using

C# 10 になって結構うれしい機能のひとつが暗黙的な using ディレクティブと global using の機能です。

docs.microsoft.com

この機能があるおかげで、ほとんどすべてのクラスファイルに追加していた System 名前空間や、いちいち追加するのがめんどくさかった System.Linq 名前空間を、何も指定せずに使える状態にしてくれます。 また同じプロジェクト内の多くのクラスで追加しなければならない名前空間も、 1 か所で定義するだけで使えるようにしてくれます*1。 コードも若干ではありますが短くできますし、非常に便利で重宝している機能です。

名前空間の削除と並び替え

コードをガシガシ書いていると、 using ディレクティブが割と汚れがちです。 並び順も意識していないとぐちゃぐちゃになりますし、コードを書いている途中でいらなくなった using ディレクティブも、気にしないでおくと削除されず、放置されやすくなります。 そんなときに Visual Studio の「Using の削除と並び替え」の機能は非常に役立ちます。

f:id:masatsuna:20220219004434g:plain
「Using の削除と並べ替え」機能

こんな感じで、使っていない using ディレクティブを削除してくれます。 また残った using ディレクティブについても、名前空間の昇順で並べ替えを行ってくれます。

暗黙的な using ディレクティブ / global using と using ディレクティブの配置場所

この機能は暗黙的な using ディレクティブや global using に対しても有効に働きます。 例えばコンソールアプリケーションのプロジェクトで、以下のように System 名前空間にある NotSupportedException クラスを使用したとします。 この時「Using の削除と並び替え」を実行すると、 System 名前空間の using ディレクティブは暗黙的な using ディレクティブの名前空間に該当するため削除されます。

f:id:masatsuna:20220219004910g:plain

ところが、暗黙的な using ディレクティブの効いている using ディレクティブや、 global using で宣言した名前空間の using ディレクティブを名前空間の宣言の内側に置くと、「Using の削除と並び替え」では削除できなくなります。 先ほどのコードで名前空間宣言の内側に System 名前空間の using ディレクティブを配置しなおし、同じ操作を行うと以下のようになります。

f:id:masatsuna:20220219112757g:plain

今回は暗黙的な using ディレクティブを用いて動作確認していますが、 global using でも同じことが起きます。

なぜ Using の削除と並び替えが動作しないのか

例えば以下のようなコードを準備しておきます。 このコードのポイントは、あえて System 名前空間にある DateTime 構造体と同じ名前にしている点です。

namespace Hoge;

public struct DateTime
{
    public override string ToString()
        => "ダミーオブジェクト";
}

以下のように実装した DoSomething メソッドを呼び出してみます。

namespace Hoge.Fuga;

using System;

public class Demo
{
    public void DoSomething()
        => Console.WriteLine(new DateTime().ToString());
}

このコードでは、名前空間宣言の内側で定義した using ディレクティブの働きにより、 System 名前空間の DateTime 構造体が使われます。 そのため、実行結果は以下のようになります。

0001/01/01 0:00:00

このコードから System 名前空間の using ディレクティブを以下のように削除します。

namespace Hoge.Fuga;

public class Demo
{
    public void DoSomething()
        => Console.WriteLine(new DateTime().ToString());
}

このコードでは、 Demo クラスの定義されている名前空間より上位の名前空間にある DateTime 構造体が優先して使われるようになります。 暗黙的な using ディレクティブで参照している DateTime 構造体は使われません。 そのため、実行結果は以下のようになります。

ダミーオブジェクト

このように、 namespace 宣言の内側に定義している using ディレクティブを削除してしまうと、もともとの実装とは異なるクラスや構造体を参照してしまう可能性があります。 「Using の削除と並び替え」で暗黙的な using ディレクティブや global using で宣言した名前空間が削除されない要因はこれです。

なお、以下のように System 名前空間の using ディレクティブを名前空間宣言の外側に置いた場合は、 Hoge 名前空間の DateTime 構造体が使われます。 よって「Using の削除と並び替え」を行うと、 System 名前空間の using ディレクティブは削除されます。

using System;

namespace Hoge.Fuga;

public class Demo
{
    public void DoSomething()
        => Console.WriteLine(new DateTime().ToString());
}

感想

意図しない不具合を巻き起こす可能性のあるコード修正を IDE が自動実行するのはよくないかもしれません。 しかし、せっかく導入された暗黙的な using ディレクティブや global using が意味のないものになってしまうのも、ちょっと寂しい気もします。

今までいろいろなアプリケーションを構築してきた中で、私は幸運にもこういったケースに遭遇することはありませんでした。 暗黙的な using ディレクティブや global using の利用を優先するためにも、名前空間の外に using ディレクティブを配置する方向でコーディングルールを整備してもよいのかもしれませんね。

気になって .NET の GitHub Issue を探してみたところ、同じようなものが報告されていたのでリンクしておきます。

https://github.com/dotnet/roslyn/issues/57729

*1:個人的には xUnit の単体テストプロジェクトで xUnit 名前空間を global using として宣言する使い方が気に入っています。