UNCパス対応時のちょっとした注意(Path.GetDirectoryName)
.NET Frameworkの殆どのIO関連クラスは共有フォルダも通常のローカルディスクと同様にアクセスできるので、UNCパスについて特に意識せずにコーディングできます。
そんな.NET Frameworkですが、最近ちょっとした落とし穴に遭ったのでメモしておきます。
(基本的にはレアケースです。)
今回注目するのは、System.IO.PathクラスのGetDirectoryName(string)メソッドです。
実はmsdnの解説にちゃんと書いてあります。
ほとんどの場合、このメソッドは、パス内の最後の DirectorySeparatorChar または AltDirectorySeparatorChar を含めずに、それよりも前にあるすべての文字で構成された文字列を返します。パスがルート ディレクトリ ("c:\" など) で構成されている場合は、null が返されます。このメソッドは "file:" を使用するパスをサポートしていません。返されるパスには、DirectorySeparatorChar または AltDirectorySeparatorChar は含まれないため、返されたパスを GetDirectoryName メソッドに再度渡すと、返された文字列に対してこのメソッドを呼び出すたびに 1 フォルダ レベルの切り捨てが行われます。たとえば、パス "C:\Directory\SubDirectory\test.txt" を GetDirectoryName メソッドに渡すと、"C:\Directory\SubDirectory" が返されます。この文字列 "C:\Directory\SubDirectory" を GetDirectoryName に渡すと、"C:\Directory" になります。
赤字の部分が注意点で、nullを返すことが明記されてますので、実際使用するときにはnullチェックが必要です。
一般的な注意点
Path.GetDirecotryNameがnullを返す場合があるのですから、当然null対応処理を入れなければいけません。
コードの流れによっては、string.Emptyや「元のパス自体」で問題ないこともあるでしょう。
その場合は次のように書けます。
- stirng folder = Path.GetDirectoryName(path) ?? string.Empty;
- stirng folder = Path.GetDirectoryName(path) ?? path;
しかし、「指定されたファイルと同一フォルダにログファイルを作成する」というシナリオの場合ちょっと困ります。
元のファイルの拡張子を変えるだけであれば、
- string logPath = Path.ChangeExtension(path, ".log");
とすれば良いのですが、ファイル名も”logfile.txt”とすると、こんな感じでしょうか。
- string logPath = Path.Combine(Path.GetDirectoryName(path), "logfile.txt");
これはnullチェックなしですが、 実際は殆どの場合意図通り動作します。
pathがルートフォルダにある場合 “C:\DataFile.dat” のようなときでも、GetDirecotryNameは “C:\” を返し、logPath は “C:\logfile.txt” になります。
ローカルディスクで問題になるのは、pathにフォルダが指定された場合です。
例えば、「指定したフォルダの並びに(同一階層に)ログフォルダを作成する」というシナリオの場合です。
シナリオにちょっと無理がありますが、 この時“C:\”を指定するのは例外ケースで、あらかじめ設定ミスのエラーとすべきだと思います。
もう一つ、「複数のデータ保存用フォルダを設定して、保存時に選択できるように」というシナリオはどうでしょう。
保存フォルダ選択時にフォルダ名を表示する訳ですが、そこでPath.GetDirectoryNameを使うとその後の処理でArgumentNullExceptionが発生しそうです。
しかし、データを”C:\”のようにルートフォルダに保存するのは心理的(*1)に避けると思われるため、実際にはあまり遭遇しないでしょう。(←潜在バグとなります)
UNCを考慮した場合の注意
「複数のデータ保存用フォルダを設定して、保存時に選択できるように」というシナリオでUNCの場合、心理的(*1)な防壁が緩くなりそうです。
目的別にサーバ上に共有フォルダを作成して保存先として複数設定する場合です。
このとき、設定値は、共有フォルダパス(\\Server\Share)になるかもしれません。
この保存先の選択時にPath.GetDirectoryNameを使用すると、その後の処理でArgumentNullExceptionを食らいます。
それを”保存先選択ダイアログ”のOnLoadでやると残念な結果になります。
(コンストラクタでやるのは問題外ですが)
ローカル絶対パスとUNCでPath.GetDirectoryNameの戻り値の違いを見てみます。
- ローカルディスク 絶対パス
- Path.GetDirectoryName("C:\first\Second\file")='C:\first\Second'
- Path.GetDirectoryName("C:\first\file")='C:\first'
- Path.GetDirectoryName("C:\first")='C:\'
- Path.GetDirectoryName("C:\")='nullです'
- Path.GetDirectoryName("C:")='nullです'
- UNC
- Path.GetDirectoryName("\\Server\share\folder\file")='\\Server\share\folder'
- Path.GetDirectoryName("\\Server\share\file")='\\Server\share'
- Path.GetDirectoryName("\\Server\share")='nullです'
- Path.GetDirectoryName("\\Server\")='nullです'
- Path.GetDirectoryName("\\Server")='nullです'
ローカルとUNCとフォルダ2つから始めて、同じように書いているのにnullになる結果が1つずれている感じがします。
(それは錯覚で、”\\Server\Share”が”C:\”と対応するので、このテスト項目がそもそもずれています)
ただ、共有フォルダのため共有名”Share”がディレクトリに見えてしまうのが原因です。
結局UNCでもローカルでも同じなのですが、心理的な理由でエラーの発生度合いが変わります。
このため、「UNCだと上手く動かない」なんて事になります。
結論
だらだら長くなってしまいましたが、結論としては、
「Path.GetDirectoryNameでフォルダを扱う場合、nullチェックをするか ?? 演算子でルートフォルダが指定された時の値を書いておこう」
ということです。
サンプルコード
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.IO;
- namespace GetDirTest
- {
- class Program
- {
- static void Main(string[] args)
- {
- Console.WriteLine("ローカルディスク 絶対パス");
- ReportToConsole(@"C:\first\Second\file");
- ReportToConsole(@"C:\first\file");
- ReportToConsole(@"C:\first");
- ReportToConsole(@"C:\");
- ReportToConsole(@"C:");
- Console.WriteLine("\nUNC");
- ReportToConsole(@"\\Server\share\folder\file");
- ReportToConsole(@"\\Server\share\file");
- ReportToConsole(@"\\Server\share");
- ReportToConsole(@"\\Server\");
- ReportToConsole(@"\\Server");
- }
- private static void ReportToConsole(string path)
- {
- Console.WriteLine(" Path.GetDirectoryName(\"{0}\")='{1}'",
- path, Path.GetDirectoryName(path) ?? "nullです");
- }
- }
- }

コメント