『C#逆引きレシピ』から開発が捗るおすすめレシピを
第04章 ステートメントと特殊な演算子
060 ネストしたループから抜け出したい
gotoステートメント
gotoステートメントは指定したラベル(末尾を「:」で終わらせた識別子)へ制御を移します。有名なgoto有害論(NOTEを参照してください)のgotoとほぼ同等の機能です。ただし、飛び先に指定するラベルは、変数の可視性ルールにしたがいます。つまり、ブロックの内側やメソッドをまたがるラベルは指定できません。
NOTE:gotoステートメントの是非
現代では、まるで「くだん(半人半牛の妖怪)」のように実際に使われているコードを見たこともないのに嫌い恐れる人が存在するのが興味深い点です。
gotoを使わずに多重ループを抜けるために、状態変数を導入して複雑なif文と多数のbreakを書くのであれば1つのgotoを記述すべきです。
for (var x = 1; x < 100; x++) { for (var y = 1; y < 100; y++) { for (var z = 1; z < 100; z++) { // xの2乗が3y+5zと等しくなったら処理を終了する if (Math.Pow(x, 2) == 3 * y + 5 * z) { Console.WriteLine($"x^2 == 3 * y + 5 * z; x={x}, y={y},z={z}"); goto found; // ラベル末尾の「:」は書かない } } } } Console.WriteLine("unknown"); // 最後までループを走ったら見つからなかったことになる found: // ラベルは識別子+「:」で記述する Console.WriteLine("end");
「私自身はループが深くならないようにメソッドへ分割するからgotoを使う機会はほとんどないのですが、多重ループからの脱出にifとbreakを各層に置いた汚いコードは目にします。そういうコードを見ると、gotoを使うほうがいいのになぁと思いますね」
第08章 クラス
160 複数の値を返すメソッドを作成したい(Tupleを使いたい)
Tupleクラスを利用する
Tupleクラスを利用すると、クラスや構造体を定義せずに型情報付きで複数の値を1つのオブジェクトとして扱えます。たとえばコレクションへの格納、メソッド引数、戻り値などです。Tupleは、複数の型と値の組み合わせを運ぶための簡易オブジェクトで、Tuple.Create静的メソッドを利用して作成します。
Tupleとは
Tuple(タプル)とは、ペア(2つ組)、トリプレット(3つ組)などを一般化した「組」で、かつ構成要素の順序づけが決まっているものを言います。例えば、本レシピのサンプルではItem1とItem2 は異なる意味を持つため順序が重要であり、tupleを用いることができます。
using System; class Calc { // 2つのint型を格納するTupleを返す。Tupleには型パラメーター<>が必須 static TupleDiv(int x, int y) { // Item1に商、Item2に剰余を保持するTupleを生成する return Tuple.Create(x / y, x % y); } static void Main() { var result = Div(81, 13); // 割り算の商と余りを表示する // Tupleが格納するオブジェクトへはItem1、Item2……プロパティでアクセスする Console.WriteLine($"81 / 13 = {result.Item1}...{result.Item2}"); } } // 出力 // 81 / 13 = 6...3
「Tupleは便利ですが、意外と実装がきれいではなくなるんですね。ですが、便利なものほどすぐに変化していくのがC#のいいところなので、どんどんブラッシュアップされていくと思います。せっかく載せたので、すぐに古くなりそうなのは悲しいですけどね」
第11章 ファイルの制御
204 安全なファイルの更新方法を知りたい
安全なファイルの更新とは
重要なファイルについては、プログラムのバグ、ディスクフルを含む実行時のディスク障害、処理対象のデータの異常などによる消失や破壊されたデータだけが残ることを避ける必要があります。
安全にファイルを更新するには、以下の手順を取ります。
- 更新対象のファイルを読み取り専用で開く
- 更新後のファイルを書き込み用に開く
- 更新は(2)のファイルに対して行う(更新処理は、(1)から(2)への更新処理を伴うフィルタリング処理とする)
- (1)のファイルをバックアップ用に名前を変更する
- (3)で処理が完了したファイルを(1)のファイルに置き換える
このようにすると、ディスクに必要となる容量は一時的に元のファイルの倍以上となりますが、どの時点で障害が発生しても、更新前のファイルを復元できます。
File.Replace静的メソッドを利用する
FileクラスのReplace静的メソッドは同一ディスクドライブ上のファイルについて上記の(4)~(5)の手順を実行します。
// 更新用ファイルは一時ファイルとする var info = new FileInfo(Path.GetTempFileName ( )); try { // 更新対象ファイルを読み込み専用でオープンする using (var reader = File.OpenText(args[0])) // 更新対象ファイルを書き込み用にオープンする using (var writer = File.CreateText(info.FullName)) { var line = string.Empty; while ((line = reader.ReadLine ( )) != null) { // 更新処理を行う writer.WriteLine(line.Replace("vb", "cs")); } } // File.Replaceは // 第1引数で指定したファイルを // 第2引数で指定したファイルに置き換える // 第3引数で指定したファイルに元の第2引数で指定したファイルはバックアップされる File.Replace(info.FullName, args[0], Path.ChangeExtension(args[0],".backup")); } finally { // 正常に終了した場合は、File.Replaceメソッドによって削除(移動)済みとなる info.Delete ( ); }
「File.Replaceの発想はいいんですが、異なるドライブのファイルを扱えない点で惜しいメソッドです。最初はFile.Replace静的メソッドを使わない方法で考えていましたが、長くなってしまうので使うことにしました。もちろん同じドライブしか使えないと記載しておきました。すべてを同じドライブで実行するのは、ディスク障害に対しては安全ではないのですが、それに目をつぶれば志が高いメソッドではあります(笑)」
第14章 LINQ
232 NameValueCollectionをLINQで使いたい
コレクションにLINQが適用できない場合はIEnumerable<T>を作る
NameValueCollectionとその派生クラスは、旧型のコレクション(IEnumerable)なのでLINQを適用できません。
このような場合以下の手順でIEnumerable<T>を作ります。
- Keys(またはAllKeys)プロパティに拡張メソッドのCast<T>をキーの型を指定して適用します。
- Castメソッドが返すIEnumerable<T>のSelectメソッドを呼び出します。Selectに与えるラムダ式で、引数に与えられたキーを元のNameValueCollectionに適用して値を取得し、キーと値のペアを格納する匿名クラスかタプルを返します。
- (2)によってキーと値のペアのIEnumerable<T>が得られるので、以降は通常通りにLINQで記述します。
// using System.Net;が必要 // URIを与えてHttpWebRequestを生成する var req = WebRequest.CreateHttp("http://example.com/"); req.UserAgent = "testagent"; req.Accept = "*/*"; // Webサーバーからレスポンスを得る using (var resp = req.GetResponse()) { // WebResponse.HeadersはWebHeaderCollectionなのでそのままではLINQを適用できない // Cast<string>を呼び出してキーのIEnumerable<string>を得る foreach (var h in resp.Headers.Keys.Cast<string>() // Selectを呼び出してキーと値の匿名クラスのオブジェクトに変換する .Select(k => new { Key = k, Values = resp.Headers.GetValues(k) }) // キーにContentが含まれるものを抽出する .Where(kv => kv.Key.IndexOf("Content") >= 0)) { // Valuesはstring[]なのでstring.Joinを使って全要素を出力する Console.WriteLine($"{h.Key}: {string.Join(",", h.Values)}"); } } // 出力(例) // Content-Length: 1270 // Content-Type: text/html
「全部LINQで書きたいのにレガシーなコレクションにLINQで書けないものがあって嫌だ、という知り合いがいたんです。これは最初私はピンとこなかったんですが、しばらくして『なんでLINQで使えないんだ?』と実際に感じることが何度かあったので、レシピとして掲載することにしました。ちなみに、LINQをSQLのように書くのは実用性皆無の単なるデモ目的だと考えているので、プロのプログラマ用の本書では紹介していません」
第15章 ネットワークと通信
247 IPアドレスとMACアドレスを取得したい
取得手順
IPアドレスとMACアドレスはコンピューターのネットワークインターフェイス(仮想を含むネットワークアダプタ)に依存します。
これを利用して次の手順でIPアドレスとMACアドレスを取得します。
- System.Net.NetworkInformation.NetworkInterfaceクラスのGetAllNetworkInterfaces静的メソッドを呼び出します。
- (1)が返したNetworkInterfaceオブジェクトの、配列の各要素について以下を実行します。この時、NetworkInterfaceTypeプロパティが、System.Net.NetworkInformation.NetworkInterfaceType列挙体のEthernet以外のものは無視します。
- GetIPPropertiesメソッドを呼び出してSystem.Net.NetworkInformation.IPInterfacePropertiesオブジェクトを取得します。
-
a. IPv4のアドレスが必要な場合は、IPInterfacePropertiesオブジェクトのGetIPv4Propertiesメソッドを呼び出します。nullでなければIPv4の情報を保持しているのでUnicastAddressesプロパティ(System.Net.NetworkInformation.UnicastIPAddressInformationクラスのコレクション)を列挙します。
b. IPv6のアドレスが必要な場合は、IPInterfacePropertiesオブジェクトのGetIPv6Propertiesメソッドを呼び出します。nullでなければIPv4の情報を保持しているのでUnicastAddressesプロパティ(System.Net.NetworkInformation.UnicastIPAddressInformationクラスのコレクション)を列挙します。 - UnicastIPAddressInformationオブジェクトのAddressプロパティを参照してSystem.Net.IPAddressクラスのオブジェクトを取得します。
- IPAddressオブジェクトのAddressFamilyプロパティを参照します。System.Net.Sockets.AddressFamily列挙体が返るので、IPv4アドレスが必要な場合はAddressFamily.InterNetworkのものを、IPv6アドレスが必要な場合はAddressFamily.InterNetworkV6のものを選択します。
- (6)で選択したIPAddressオブジェクトのToStringメソッドを呼び出して可読表現を入手します。
-
MACアドレスを入手するには、(4)a.または(4)b.で選択されたNetworkInterfaceオブジェクトを利用します。
a. GetPhysicalAddressメソッドを呼び出してSystem.Net.NetworkInformation.PhysicalAddressオブジェクトを取得します。
b. PhysicalAddressオブジェクトのToStringメソッドを呼び出して可読表現を入手します。
// using System.Net.NetworkInformation; using System.Net.Sockets;が必要 // すべてのネットワークアダプタを列挙する foreach (var intf in NetworkInterface.GetAllNetworkInterfaces() // イーサネットタイプのものだけを選択する .Where(itf => itf.NetworkInterfaceType == NetworkInterfaceType.Ethernet) // 後続の処理が楽になるように必要な情報のみのオブジェクトを作成する .Select(itf => new { Name = itf.Name, MacAddress = itf.GetPhysicalAddress(), IPProperties = itf.GetIPProperties() }) // IPアドレスを持つかどうかを判定する // GetIPv?Propertiesはそれぞれ返す型が異なるので本来は??では接続できない // ここではnullかどうかの判定なのでobjectにキャストして同時に判定している .Where(e => ((object)e.IPProperties.GetIPv4Properties() ?? (object)e.IPProperties.GetIPv6Properties()) != null)) { // アダプタ名とMACアドレスを出力する Console.WriteLine($"Name: {intf.Name}, MACAddress: {BitConverter. ToString(intf.MacAddress.GetAddressBytes())}"); // IPv4アドレスを表示する foreach (var address in intf.IPProperties.UnicastAddresses .Select(e => e.Address) .Where(e => e.AddressFamily == AddressFamily.InterNetwork)) { Console.WriteLine($"IPv4 Address: {address}"); } // IPv6アドレスを表示する foreach (var address in intf.IPProperties.UnicastAddresses .Select(e => e.Address) .Where(e => e.AddressFamily == AddressFamily.InterNetworkV6)) { Console.WriteLine($"IPv6 Address: {address}"); } } // 出力例 // Name: vEthernet (Hyper-V virtual interface), MACAddress: 00-xx-xx-xx-xx-xx // IPv4 Address: 192.168.253.10 // IPv6 Address: xxxx::xxxx:xxxx:xxxx:xxxx%yy
GetUnicastAddressesメソッドを利用する方法
.NET Framework 4以降であれば次のプログラムで示すように、利用可能なUnicastAddressInformationのコレクションをSystem.Net.NetworkInformation.IPGlobalPropertiesクラスのGetUnicastAddressesメソッドで取得できます。アダプタ情報等が不要な場合はこの方法が簡便です。
// using System.Net.NetworkInformation; using System.Net.Sockets; が必要 var address = IPGlobalProperties.GetIPGlobalProperties() .GetUnicastAddresses() // 以降の処理用にIPAddressオブジェクトの // 列挙に変換する .Select(e => e.Address) // IPv4にここでは限定する(IPv6アドレスが // 必要であれば条件を変える) .Where(e => e.AddressFamily == AddressFamily.InterNetwork) // この方法はループバックアドレス(127.0.0.1)が // 含まれるので除外する .Where(e => !IPAddress.IsLoopback(e)) // マルチホームコンピューターの場合は他の条件も要考慮 .First(); Console.WriteLine($"IPv4 Address: {address}");
「これは力作です。苦労してサンプルコードを書いたんですが、紹介しているように.NET Framework 4からは短いコードで実現できるんですよ。.NET Flameworkではよく使う機能はどんどん変化していくので、きちんとキャッチアップしていかないとたいへん損をします。新しいことを覚えて短く書くのが得なのか、いま知っている長いコードを以前のソースからコピペしてくるほうが省エネなのか、判断は難しいのですが、どこかで新しいものに更新していかないと、C#と付き合っている限り、今後もその古いコードを使ってしまいますからね。気がついたら知識もコードも更新していきましょう」