私は C#4.0に関するアンダースの話とC#5.0のスニークプレビュー を見ていて、C#でオプションのパラメーターが利用できるとき、そうでないメソッドを宣言するための推奨される方法は何かを考えさせられましたすべてのパラメータを指定する必要がありますか?
たとえば、FileStream
クラスのようなものには、約15の異なるコンストラクターがあり、論理的な「ファミリー」に分割できます。以下は文字列からのもの、IntPtr
からのもの、SafeFileHandle
からのもの。
FileStream(string,FileMode);
FileStream(string,FileMode,FileAccess);
FileStream(string,FileMode,FileAccess,FileShare);
FileStream(string,FileMode,FileAccess,FileShare,int);
FileStream(string,FileMode,FileAccess,FileShare,int,bool);
このタイプのパターンは、代わりに3つのコンストラクターを使用し、デフォルトのパラメーターにオプションのパラメーターを使用することで簡略化できるように思えます。これにより、コンストラクターの異なるファミリーがより明確になります[注:この変更は行われないことを知っていますBCLで作成された、私はこの種の状況について仮説的に話している]。
どう思いますか? C#4.0以降では、コンストラクターとメソッドの密接に関連するグループをオプションのパラメーターを持つ単一のメソッドにする方が理にかなっていますか?それとも、従来の多くのオーバーロードメカニズムに固執する十分な理由がありますか?
以下を検討します。
デフォルトがどのように機能するかは確認していませんが、const
フィールドへの参照と同じように、デフォルト値が呼び出しコードに組み込まれると想定しています。通常は問題ありません-とにかくデフォルト値の変更はかなり重要です-しかし、それらは考慮すべきことです。
メソッドのオーバーロードが通常、異なる数の引数で同じことを実行する場合、デフォルトが使用されます。
メソッドのオーバーロードがパラメーターに基づいて異なる機能を実行する場合、オーバーロードは引き続き使用されます。
私はVB6日にオプションのバックを使用しましたが、それを逃してしまったため、C#でのXMLコメントの重複が大幅に減少します。
Delphiとオプションのパラメータをずっと使ってきました。代わりにオーバーロードの使用に切り替えました。
より多くのオーバーロードを作成する場合、オプションのパラメーターフォームと常に競合するため、とにかくそれらを非オプションに変換する必要があるためです。
そして、私は一般に1つのsuperメソッドがあり、残りはそのメソッドのより単純なラッパーであるという概念を気に入っています。
私は間違いなく4.0のオプションのパラメーター機能を使用します。それはとんでもないことを取り除きます...
public void M1( string foo, string bar )
{
// do that thang
}
public void M1( string foo )
{
M1( foo, "bar default" ); // I have always hated this line of code specifically
}
...そして、発信者がそれらを見ることができる場所に値を配置します...
public void M1( string foo, string bar = "bar default" )
{
// do that thang
}
はるかにシンプルで、エラーが発生しにくくなります。私は実際にこれを過負荷ケースのバグとして見ました...
public void M1( string foo )
{
M2( foo, "bar default" ); // oops! I meant M1!
}
私はまだ4.0コンパイラーで遊んでいませんが、コンパイラーが単にオーバーロードを出力するだけであることを知ってショックを受けることはありません。
オプションのパラメーターは基本的にメタデータの一部であり、メソッドコールを処理するコンパイラーに、呼び出しサイトで適切なデフォルトを挿入するように指示します。対照的に、オーバーロードは、コンパイラーが多数のメソッドの1つを選択できる手段を提供します。その一部はデフォルト値を提供する場合があります。サポートしていない言語で書かれたコードからオプションのパラメーターを指定するメソッドを呼び出そうとすると、コンパイラーは「オプション」パラメーターを指定する必要がありますが、オプションのパラメーターを指定せずにメソッドを呼び出すのはデフォルト値と等しいパラメーターで呼び出すのと同じで、そのような言語がそのようなメソッドを呼び出すのに障害はありません。
呼び出しサイトでのオプションのパラメーターのバインドの重要な結果は、コンパイラーで使用可能なターゲットコードのバージョンに基づいて値が割り当てられることです。アセンブリFoo
にデフォルト値5のメソッドBoo(int)
があり、アセンブリBar
にFoo.Boo()
への呼び出しが含まれている場合、コンパイラはそれをFoo.Boo(5)
として処理します。デフォルト値が6に変更され、アセンブリFoo
が再コンパイルされる場合、Bar
は、新しいバージョンのFoo
で再コンパイルされない限り、または再コンパイルされるまでFoo.Boo(5)
を呼び出し続けます。したがって、変更される可能性のあるものにオプションのパラメーターを使用することは避けてください。
オプションの引数またはオーバーロードを使用する必要があるかどうかは議論できますが、最も重要なのは、それぞれに置き換えができない独自の領域があることです。
オプションの引数を名前付き引数と組み合わせて使用すると、COM呼び出しのいくつかのlong-argument-lists-with-all-optionalsと組み合わせると非常に役立ちます。
メソッドが多くの異なる引数型(例の1つにすぎない)を操作でき、内部でキャストを行う場合など、オーバーロードは非常に便利です。意味のあるデータ型をフィードするだけです(既存のオーバーロードで受け入れられます)。オプションの引数でそれを打つことはできません。
オプションのパラメーターの私のお気に入りの側面の1つは、メソッド定義に移動しなくても、パラメーターを指定しないとパラメーターがどうなるかを確認できることです。 メソッド名を入力すると、Visual Studioはパラメーターのデフォルト値を表示します。オーバーロードメソッドを使用すると、ドキュメントを読む(利用できる場合でも)か、メソッドの定義(利用できる場合)およびオーバーロードがラップするメソッドに直接移動するのに悩まされます。
特に、ドキュメント化の作業はオーバーロードの量とともに急速に増加する可能性があり、おそらく既存のオーバーロードから既存のコメントをコピーすることになります。これは値を生成せず、 DRY-principle )を壊すので、非常に迷惑です。一方、オプションのパラメーターを使用すると、すべてのパラメーターが文書化されて正確に1か所になり、それらの意味とデフォルトが表示されます入力中の値。
最後に重要なことですが、APIを利用している場合は、実装の詳細を検査するオプションがなく(ソースコードがない場合)、オーバーロードされたスーパーメソッドを確認する機会がありません。包んでいます。したがって、ドキュメントを読んですべてのデフォルト値がそこにリストされていることを期待して立ち往生していますが、これは常にそうであるとは限りません。
もちろん、これはすべての側面を扱う答えではありませんが、これまでカバーされていなかったものを追加すると思います。
デフォルトの方がメソッドに近いので、オプションのパラメーターを期待しています。したがって、「expanded」メソッドを呼び出すだけの数十行のオーバーロードの代わりに、メソッドを1回定義するだけで、オプションのパラメーターのデフォルトがメソッドシグネチャに表示されます。私はむしろ見たい:
public Rectangle (Point start = Point.Zero, int width, int height)
{
Start = start;
Width = width;
Height = height;
}
これの代わりに:
public Rectangle (Point start, int width, int height)
{
Start = start;
Width = width;
Height = height;
}
public Rectangle (int width, int height) :
this (Point.Zero, width, height)
{
}
明らかにこの例は非常に単純ですが、5つのオーバーロードを伴うOPの場合、事態はすぐに混雑する可能性があります。
オプションパラメータの注意点の1つはバージョン管理です。この場合、リファクタリングは意図しない結果をもたらします。例:
初期コード
_public string HandleError(string message, bool silent=true, bool isCritical=true)
{
...
}
_
これが上記のメソッドの多くの呼び出し元の1つであると想定します。
_HandleError("Disk is full", false);
_
ここではイベントはサイレントではなく、クリティカルとして扱われます。
ここで、リファクタリング後にすべてのエラーでユーザーにプロンプトが表示されたことがわかり、サイレントフラグが不要になったとします。それを削除します。
リファクタリング後
前の呼び出しはまだコンパイルされており、変更せずにリファクタリングを通過するとします。
_public string HandleError(string message, /*bool silent=true,*/ bool isCritical=true)
{
...
}
...
// Some other distant code file:
HandleError("Disk is full", false);
_
これでfalse
は意図しない影響を及ぼし、イベントはクリティカルとして扱われなくなります。
コンパイルエラーや実行時エラーがないため、これは微妙な欠陥をもたらす可能性があります( this や this などのオプションのその他の警告とは異なります)。
この同じ問題には多くの形式があることに注意してください。他の1つのフォームの概要 ここ 。
また、メソッドを呼び出すときに名前付きパラメーターを厳密に使用すると、次のような問題が回避されます:HandleError("Disk is full", silent:false)
。ただし、他のすべての開発者(またはパブリックAPIのユーザー)がそうすることを想定するのは実際的ではない場合があります。
これらの理由により、他の説得力のある考慮事項がない限り、オプションのパラメーターをパブリックAPI(または、広く使用されている場合はパブリックメソッド)で使用することは避けます。
オプションのパラメーター、メソッドのオーバーロードには、それぞれ長所と短所があります。どちらを選択するかは、ユーザーの好みによって異なります。
オプションパラメータ:.Net 4.0でのみ使用できます。オプションのパラメーターはコードサイズを減らします。 outおよびrefパラメーターを定義することはできません
オーバーロードされたメソッド:Outおよびrefパラメーターを定義できます。コードサイズは増加しますが、オーバーロードされたメソッドは簡単に理解できます。
オプションの代わりにオーバーロードを使用する場合に、非常に簡単です。
一緒にしか意味をなさないパラメータが多数ある場合は、それらにオプションを導入しないでください。
または、より一般的には、メソッドシグネチャが意味のない使用パターンを有効にする場合は常に、可能な呼び出しの順列の数を制限します。たとえば、オプションの代わりにオーバーロードを使用する(このルールは、同じデータ型のパラメーターが複数ある場合にも当てはまります。ここでは、ファクトリメソッドやカスタムデータ型などのデバイスが役立ちます)。
例:
enum Match {
Regex,
Wildcard,
ContainsString,
}
// Don't: This way, Enumerate() can be called in a way
// which does not make sense:
IEnumerable<string> Enumerate(string searchPattern = null,
Match match = Match.Regex,
SearchOption searchOption = SearchOption.TopDirectoryOnly);
// Better: Provide only overloads which cannot be mis-used:
IEnumerable<string> Enumerate(SearchOption searchOption = SearchOption.TopDirectoryOnly);
IEnumerable<string> Enumerate(string searchPattern, Match match,
SearchOption searchOption = SearchOption.TopDirectoryOnly);
多くの場合、オプションのパラメーターを使用して実行を切り替えます。例えば:
_decimal GetPrice(string productName, decimal discountPercentage = 0)
{
decimal basePrice = CalculateBasePrice(productName);
if (discountPercentage > 0)
return basePrice * (1 - discountPercentage / 100);
else
return basePrice;
}
_
ここの割引パラメーターは、if-then-elseステートメントにフィードするために使用されます。認識されなかった多態性があり、if-then-elseステートメントとして実装されました。このような場合、2つの制御フローを2つの独立したメソッドに分割する方がはるかに適切です。
_decimal GetPrice(string productName)
{
decimal basePrice = CalculateBasePrice(productName);
return basePrice;
}
decimal GetPrice(string productName, decimal discountPercentage)
{
if (discountPercentage <= 0)
throw new ArgumentException();
decimal basePrice = GetPrice(productName);
decimal discountedPrice = basePrice * (1 - discountPercentage / 100);
return discountedPrice;
}
_
このようにして、割引なしで電話を受けることからもクラスを保護しました。その呼び出しは、発信者が割引があると考えていることを意味しますが、実際には割引はまったくありません。そのような誤解は簡単にバグを引き起こす可能性があります。
このような場合は、オプションのパラメーターを使用せずに、呼び出し元が現在の状況に適した実行シナリオを明示的に選択するようにします。
この状況は、nullになる可能性のあるパラメーターがあることに非常に似ています。これは、実装がif (x == null)
のようなステートメントに変形する場合も同様に悪い考えです。
これらのリンクの詳細な分析を見つけることができます: オプションのパラメーターの回避 および ヌルパラメーターの回避
それらは(おそらく?)APIをゼロからモデル化するために利用できる概念的に同等の2つの方法ですが、実際に古いクライアントのランタイム下位互換性を検討する必要がある場合、残念ながら微妙な違いがあります。私の同僚(Brentに感謝!)が私にこれを指摘しました 素晴らしい投稿:オプションの引数によるバージョン管理の問題 。それからの引用:
オプションのパラメーターが最初にC#4に導入された理由は、COM相互運用性をサポートするためでした。それでおしまい。そして今、私たちはこの事実の完全な影響について学んでいます。オプションのパラメーターを持つメソッドがある場合、コンパイル時の重大な変更を引き起こす恐れがないため、追加のオプションのパラメーターを持つオーバーロードを追加することはできません。また、これは常にランタイムの重大な変更であるため、既存のオーバーロードを削除することはできません。インターフェースのように扱う必要があります。この場合の唯一の手段は、新しいメソッドを新しい名前で作成することです。したがって、APIでオプションの引数を使用する場合は、このことに注意してください。