22
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

C# その2 Advent Calendar 2019

Day 20

知っててもあまり役に立たない構文私的六選

Last updated at Posted at 2019-12-19

前口上

これは、C# その2 Advent Calendar 2019の20日目の記事です。

個人的にC#を使っていて、こんな機能有ったんだとか、こんなコトできたんだみたいなあんまり役に立たないけどクスリと笑えそうなモロモロをセレクトしてみました。

なので、既知だよ~って言う突っ込みは無しの方向で一つ、ご笑覧頂ければ幸い

割とどこにでも書けるconst

さて、1つ目はconstに関するお話。constが引き起こすあれやこれやは別として、以下のように書けちゃったりする。

using System;

namespace Advent
{
	class Program
	{
		static void Main(string[] args)
		{
			const string helloWorld = "hello world";
			Console.WriteLine(helloWorld);
		}

		public static int SomeProperty
		{
			get
			{
				const int allAns=42;
				return allAns;
			}
			set
			{
				const string header = "Your input is:";
				Console.WriteLine(header + value);
				//本来はStore処理をすべきだけど省略
			}
		}
	}
}

このように、クラスや構造体のフィールドだけでは無く、メソッドやプロパティの中にも書ける。

ちょっとしたベンチマークテスト書きたいときとか、constでパス指定とかしたり、iteration何かを定数化しておくと、わざわざ書き直す必要も無いし、変数と違って途中で書き換えが出来ないのでわりかし便利かも知れない。

使える場所が超限定の::演算子

C++で完全修飾名にするときとか、std::coutみたいに書くけど、これに近いことが出来る…けどかなり用途先が限定されている。

実例は以下の通り。

using txt=System.Text;
using sys=System;
using io=System.IO;


namespace Advent
{
	class Program
	{
		static void Main(string[] args)
		{
			const string inputText = "Hello C#!";
			var buff = new txt::StringBuilder();

			{
				using var stream = new io::StringWriter(buff);
				stream.WriteLine(inputText);
			}

			sys::Console.WriteLine(buff.ToString());
		}
	}
}

かなり回りくどいけど、usingエイリアスディレクティブを使って作成した名前空間のエイリアスに限り::を使って修飾が可能になる。なので、System::Consoleみたいなことは書けないので注意。ただこれ、当然一般的なメンバアクセス演算子.使って、new txt.StringBuilder()でも全然イケてしまうので通常はそっち使った方が良いと思う。

じゃあなんでこれが必要かというと、名前被っちゃったときの対策とのこと。実例は以下

using sys=System;


namespace Advent
{

	//命名則に反してるけど笑って許して
	public class sys
	{
		public class Console
		{
			public static void WriteLine(string value) =>
				System.Console.WriteLine($"This is \"sys.Console\" class's WriteLine:{value}");
		}

	}

	class Program
	{
		static void Main(string[] args)
		{
			sys::Console.WriteLine("hello C#!");
			sys.Console.WriteLine("hello C#!");
            
            //output
            //hello C#!
			//This is "sys.Console" class's WriteLine:hello C#!
		}
	}
}

命名則としてどーよというのは激しくあるけど、早い話上記みたいなシナリオでどっちを呼ぶのか弁別する必要があるので、この演算子が必要だったとのこと。また、子細は以下に譲るけど、唯一この演算子でしか解決できないモノにglobal::ってものがある。

ただ、繰り返しになるけどエイリアスはソースコードのファイル単位のスコープしか持ってないので、こんなことしなければならないって言うのは、割と問題があるケースが多いと思うし、書くなら書いたで書いた理由は説明できないとマズいかなとは思う。

using static

C#のver6から導入されたクラスや構造体の静的メソッド/プロパティを修飾無しで呼び出せるdirectiveだけど、実はこれ、それだけじゃ無く、指定されたクラスや構造体の入れ子になっているクラス/構造体も修飾無しで呼び出せる。

下記のようなアセンブリがあったとして

namespace ExternalLib
{
	public static class Envelope
	{
		public class Some
		{
		}

		public static class Nested
		{
			public static int Add(int x, int y) => x + y;
		}

		public static int Calc(int x, int y) => x / y;

	}

}

こいつを、参照してるコンソールアプリあたりでこんな風に書ける。

using System;
using static ExternalLib.Envelope;

namespace Advent
{
	static class MainEntry
	{
		public static void Main()
		{
			Console.WriteLine(Nested.Add(10, 20));
			Console.WriteLine(Calc(100, 20));
			var some = new Some();
		}
	}
}

最初、文字通り、静的な諸々のみが修飾無しで呼び出せるだけだと考えていたので、ちょっとびっくりした。また、当然入れ子になったクラス/構造体に対してもusing staticは使える。

using System;
using static ExternalLib.Envelope;
using static ExternalLib.Envelope.Nested;
namespace Advent
{
	static class MainEntry
	{
		public static void Main()
		{
			Console.WriteLine(Add(10, 20));
			Console.WriteLine(Calc(100, 20));
		}
	}
}

使い道としては、割と局所的だし、こじつけっぽいけどメソッドの階層化とかにはちょっとだけ使えるかもとは思ったり。

using System;
using System.Collections.Generic;
using System.Linq;

namespace ExternalLib
{
	public static class Calculate
	{
		public static class Integer
		{
			public static int Add(int x, int y) => x + y;
		}

		public static class Triangle
		{
			public static double Sin(double a) => Math.Sin(a);
		}

		public static class Statistics
		{
			public static double Average(IEnumerable<double> collection) => collection.Average();
		}
	}
}

こんな風に書いておけば

using System;
using System.Linq;
using static ExternalLib.Calculate;
using static System.Linq.Enumerable;

namespace Advent
{
	static class MainEntry
	{
		public static void Main()
		{
			Console.WriteLine(Integer.Add(10, 20));
			Console.WriteLine(Triangle.Sin(0.733));
			Console.WriteLine(Statistics.Average(Range(0, 200).Select(x => (double) x)));

		}
	}
}

こんな感じ書ける。

ただまぁ、何かそれほどありがたみが無いなぁって感じが強い。

new付きの諸々

C#にはnew修飾子ってのがあり、有り体に言えば継承された諸々を明示的に隠蔽することが出来る。で、これ別にvirtualのついて無いメソッド/プロパティだけじゃ無くありとあらゆるモノが隠蔽できる。

using System;

namespace Advent
{
	public class Base
	{

		public class Nested
		{
			public Nested() => Console.WriteLine("Base.Nested");
		}

		public int IntField = 100;
		public int Hoge() => 42;
	}

	public class Derived : Base
	{
		public new class Nested
		{
			public Nested() => Console.WriteLine("DerivedA.Nested");
		}

		public new int IntField = 200;
		public new int Hoge() => 114514;
	}





	static class MainEntry
	{
		public static void Main()
		{
			Base up(Derived d) => d;

			var a = new Derived();

			Console.WriteLine(a.IntField);
			//200

			Console.WriteLine(up(a).IntField);
			//100

			a.IntField = 42;

			Console.WriteLine(a.IntField);
			//42

			Console.WriteLine(up(a).IntField);
			//100

			up(a).IntField = 114514;

			Console.WriteLine(a.IntField);
			//42

			Console.WriteLine(up(a).IntField);
			//114514


			//当たり前だけど違う型なので変数も別々
			var b = new Base.Nested();
			//Base.Nested

			var c = new Derived.Nested();
			//Derived.Nested
		}
	}
}

メソッドやらプロパティでも割と混乱するけど、フィールドになるとそいつに拍車がかかる。名前は一緒だけど別々に割りあてられてるメンバフィールドって言う扱いになるので、どー修飾されたかで結果が変わるという地獄絵図にw

同様に、Nestedクラスに関してもBase.Nestedと、Derived.Nestedが別個の実体として存在するので、管理が面倒なことになる。

この隠蔽に関しては、利用側が隠蔽されているのか否かをすぐにわかりずらく、また一般とは違うメンバールックアップが行われるので余程強い理由があるときに限り使うべきだと思う。

特にフィールドに対する隠蔽は同名のフィールドが別々に存在するというどー見ても地雷踏みそうな状況を作るので、余程のことがあったとしても使うべきじゃないし、こんなことするなら設計が歪んでいる方をまず疑うべきじゃ無いかと思う。

構造体の部分初期化

ローカル変数を初期化しないまま参照しようとした場合、コンパイルエラーになるけど、フィールドに限っては部分初期化に対応している。以下のように書いた場合

using System;

namespace Advent
{

	static class MainEntry
	{
		public static void Main()
		{
			ValueTuple<int, int> point;

			point.Item1 = 100;
			point.Item2 = point.Item1 + 200;

			Console.WriteLine(point);
		}
	}
}

13行目でコンパイルエラーになることは無い。

但し、これはフィールドに限った話で、部分初期化しかされていない状態で、直接関係の無いメソッドやプロパティの呼び出しを行うと,いつも通りCS1065が発生する。

namespace Advent
{
	public struct IntegerPoint
	{
		public  int X;

		public void Some() { }


		public int Y { get; set; }
	}


	static class MainEntry
	{
		public static void Main()
		{
			IntegerPoint point;

			//これは出来る。
			point.X = 100;

			//property setterの呼び出しはNG(CS0165発生)
			point.Y = 100;

			//当然、何も関係なくてもメソッドの呼び出しもNG(CS0165発生)
			point.Some();
		}
	}
}

用途としては、まさにさっきのタプルの例のように、一部を初期化して、初期化済みの値を使いつつ他のフィールドの初期化を行う時には少し使い道があるかな?と思える程度。

ただ、とち狂ったことしても問題が局所化されやすいしコンパイルエラーになるので、危険性はそこまで無いかなとは思ったり。

namespaceにくるまれていないモノ達

VisualStudioであれ、Riderであれ新たにソースコードファイルを作成すると、規定の名前空間に従った名前空間が作成されてその空間の中に各種実装を書いていくのが一般的じゃ無いかなと思う。

それじゃ、名前空間にくるまず以下のようなモノを書いたとして

public class NoEnveloped
{ }

こいつを、同一プロジェクトの名前空間にくるまっている先から呼び出したり、参照している外部アセンブリからどのように見えるかというと、実はusingすら不要で可視になる

using System;


public class OutOfNameSpace
{

}


namespace Advent
{
	static class MainEntry
	{
		public static void Main()
		{
			//こいつはExternalLibなる別のアセンブリにある
			NoEnveloped hoge = new NoEnveloped();

			//こいつは上のやつ
			OutOfNameSpace piyo = new OutOfNameSpace();
		}
	}
}

と、このように、名前空間の修飾無しで呼べてしまう。このように、名前空間に含まれていない実装はグローバル名前空間という名前空間に存在することになる。

じゃあ、以下のようなシナリオはどうなるだろうか?

using System;


public class OutOfNamespace
{

}


namespace Advent
{
	public class OutOfNamespace
	{

	}

	public class NoEnveloped
	{

	}



	static class MainEntry
	{
		public static void Main()
		{
			//この場合、Advent.NoEnvelopedと解釈される
			NoEnveloped hoge = new NoEnveloped();

			//これも同様に、Advent.OutOfNameSpaceと解釈される
			OutOfNamespace piyo = new OutOfNamespace();

			//これがグローバル名前空間にあるOutOfNamespace
			global::OutOfNamespace foo=new global::OutOfNamespace();

			//こいつは、ExternalLibにあるグローバル名前空間にいたNoEnveloped
			global::NoEnveloped bar=new global::NoEnveloped();
		}
	}
}

名前が衝突してコンパイルエラーに成るのでは無く、同一名前空間へのルックアップが優先されて、修飾無しの場合はAdvent.NoEnvelopedAdvent.OutOfNameSpaceと解釈される。

それでは、名前空間の外にいる諸々を呼び出したい場合、先の::のくだりで出ていた、global::を使って修飾することで指定可能となっている。

で、こんなことは現時点における自分の思慮では絶対にやっちゃいけないと思う。名前空間は多数のアセンブリを参照する上で、名前の衝突を防止する強力な手段なのにそいつをバイパスしちゃうのは極めてマズいし、global::で弁別可能とは言え、混乱の原因になるのでその観点からもマズいと思う。

切口上

Advent Calendarにあわせて、C#で遊んでいて*おや?*と思ったことをつらつらと書き連ねてみました。

タイポしてたことに、コンパイル実行後に気づいて**アレ通っちゃった?!**となるパターンと、.NET Coreのコードリードしていて、あらまこんな書き方出来たんだってコトが今年結構有ったので、サクッと読んでクスリと笑ってもらえるかなって感じで書いてみました。

とは言え、グローバル名前空間や、new隠蔽に関しちゃ結構危険な匂いがするので積極的に使うもんでも無いと思うし、逆にローカルconstあたりは知っていればもっと早くから使いたかったなんてこともあったりなかったり。

来年も読んで頂いた皆様が素晴らしいコーディングライフを送れることを祈念しつつ、

Merry Christmas!&Happy New Year!

22
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?