限定しりとり

 こんにちは。c#初心者兼、「java始めました」です。
 プロパティ(文法)がなくて、わざわざget/setと括弧をつけないといけないし、finalつけないと勝手にオーバーライドされるかもだし、演算子定義できないし、ジェネリックは弱いし、…etcの代わりに、staticやfinalでやたらとインライン展開されて、凄いと感心している最中です。

 さて、クラスのメソッド設計なのですが、クラスやメソッドのあり方についての質問です。
 キーが一意にノード(値)と結びついているコレクションクラスを作成中なのですが、ちょっと問題発生です。
 Setや、javaで言うMap、c#で言うDictionaryクラスには追加メソッド(addとかput)がありますが、その戻り値はvoid型(javaなら値の型もあるかな)が多いと思います。
 そのため、キーが重複した際の処理はjavaは上書き、c#は例外(インデクサは上書き)となっており、一長一短です。
 どちらの方式でも使用側が存在するかどうかのチェックを先に行えばよいのですが、使用側のコードが増えるし、どうせ今から作るクラスならもっと便利なものを作ろうという予定です。
 用意する予定の追加関連のメソッドは、
 (1)(c#風)追加を試み、成功時には生成されたノードを、キー重複の場合には例外をスローするメソッドadd(K key, V value) / Add(TKey key, TValue)、
 (2)追加を試み、成功時には生成されたノードを、キー重複の場合にはnullを返すメソッドtryAdd(K key, V value) / TryAdd(TKey key, TValue)、
 (3)(java風)追加を試み、キーが存在しない場合にはそのまま追加し、重複の場合は上書きし、どちらの場合も最後に生成されたノードを返すset(K key, V value) / Set(TKey key, TValue)、
 (4)追加を試み、成功時には生成されたノードを、キー重複の場合には既存のノードを返すメソッドtryGetAdd(K key, V value) / TryGetAdd(TKey key, TValue)、

 と、ここまでを振り返って思ったことですが、(2)のtryAdd/TryAddが(1)の(使用者側から見て)ほとんど上位互換になっているということです。
 機能が酷似し、チェックの方法が異なるだけで、非常に特殊な場合では(2)はチェックなしでも安全に利用可能なので(1)の利用価値が希薄です((1)にはチェックを半強制させるという安全面でのメリットがないわけではないですが…)。
 もちろん、addという分かりやすいメソッドがあったほうが安心する利用者はいると思いますし、インターフェイスの視点からもあった方がいいとおもいます。
 それでもデフォルトで、一番使われそうなadd/Addメソッドがあまり使われないようなクラスというのはいかがなものなのでしょうか?
 大丈夫ならそれでいいのですが、不自然、メソッドがややこしすぎる、などはないでしょうか?
 どなたでも気づいたことがあればご指摘ください。

A 回答 (5件)

> しかし、javaのデフォルトのライブラリを使っていて驚いたことが、「例外のエラーメッセージとメソッ


> ドのパラメータ名が一致しない」、「そのうえ、そんな値のパラメータを渡していない」、「そもそも、
> そんなパラメータ名の値を渡していない」ということです。
>  原因はおおよそ見当付いていますが…。
>  Addをそのまま使っていると、似たような混乱を生みかねないので、値を追加できなくて、”正しい(分
> かりやすい)”例外を出すためにTryAddを使おうという考えです。

なるほど、JDKのどのクラスのメソッドが該当するのか全く把握していませんが、
そんなのがあるんですね。

そういった正当な理由がある上で『あえて』捕捉するならば、パフォーマンス改善の
考え方も頷けますし、アリと思います。
が、きっとそれはライブラリなどのライブラリ・フレームワーク側が実装する方法であって、
それを利用して開発を行う側では、Add()、TryAdd()の利用用途が変わる場合もあると思いますよ。
(まあ、主にそれが実装スタイルの思想や要件によるでしょうけど)
    • good
    • 0
この回答へのお礼

 遅くなりました。回答ありがとうございます。
 (自作例外クラスがそこそこ改良できたので、リファクタリングに追われて、すっかり忘れていました)
 そうですね。いろいろな使い方、考え方の人がいるのですから、それぞれのニーズに合ったクラス設計が大切ですよね。
 お騒がせしました。この度はありがとうございました。

お礼日時:2013/10/17 13:38

んと、恐らく思想によると思います。


私は
『意図して例外を発生させる』なら、try catchは使わない。
『意図していない時に例外を発生させる』なら、try catchを使う。
です。
上記思想の下に適切な命令方法を優先し、速度を最重要視することはしません。


例えば重複キーがないであろう前提、もしくは重複キーがある場合には
特別なエラーという挙動を考える時に、
try {
 dictionary.Add(key, value);
} catch (Exception e) {
 throw new Exception("なんか固定のエラーメッセージ");
}
みたいなことをしているならば、それはtry catchに頼るべきではなく、
ContainsKeyなどで検証します。(多少速度が遅かろうと関係ない)
try catchは、想定外、他命令で捕捉不可能な場合のみ利用します。
if (dictionary.ContainsKey(key)) {
 throw new Exception("なんか固定のエラーメッセージ");
}
dictionary.Add(key, value);


で、その流れの考えでいくと

> if ( dictionary.TryAdd(key, value) == null )
>  throw new ~Exception(……);
と、例外発生ではなく
if ( dictionary.TryAdd(key, value) == null ) {
 ~ なにか別な処理 ~
}
『なにか別な処理』が必要な場合にはこの記述でいいのではないでしょうか。
値が設定できなくて例外にするなら、TryAdd()をする必要ありませんよね。

いずれにしても、少なからず『Try』する必要がないロジックも存在します。
その時に『Try』するのは個人的にはナンセンスです。
C#で言ったら、絶対的にパースできると分かり切っているロジックに対しても
いちいちint.TryParse()しているようなものなので。
速度ではなく、ロジックの見せ方と、例外の扱い方として。

色んな人のやり方があるわけですし、両方あっても別にいいのでは?
    • good
    • 0
この回答へのお礼

 回答、ご指摘ありがとうございます。

> 色んな人のやり方があるわけですし、両方あっても別にいいのでは?
 確かにその通りです。これは前述の通り納得・解決しました。

 ちょっと、誤解がありそうなので補足。
> 『なにか別な処理』が必要な場合にはこの記述でいいのではないでしょうか。
> 値が設定できなくて例外にするなら、TryAdd()をする必要ありませんよね。
 一般的にはその通りです。しかし、javaのデフォルトのライブラリを使っていて驚いたことが、「例外のエラーメッセージとメソッドのパラメータ名が一致しない」、「そのうえ、そんな値のパラメータを渡していない」、「そもそも、そんなパラメータ名の値を渡していない」ということです。
 原因はおおよそ見当付いていますが…。
 Addをそのまま使っていると、似たような混乱を生みかねないので、値を追加できなくて、”正しい(分かりやすい)”例外を出すためにTryAddを使おうという考えです。

お礼日時:2013/10/09 18:26

> そのクラスでAddしたときに例外が出たときに、適切なエラーメッセージにならないことが多いので、


> TryAddを使うことが多いんですね。
>  try-catchならAddでもいいのですが、ちょとだけ行が増えるので敬遠気味です。

エラーメッセージなどの『例外ではない挙動』を必要とする処理をするならば
try catchではなくContainsKey()などを利用すべきではないでしょうか。
    • good
    • 0
この回答へのお礼

 再びの回答、ご指摘ありがとうございます。

 確かに、

if ( dictionary.ContainsKey(key) )
  throw new ~Exception(……);
else Add(key, value);

 などでも
if ( dictionary.TryAdd(key, value) == null )
  throw new ~Exception(……);
 や、try-catchでAddを使うことの代用には動作的に十分だと思います。
 ただし、これではContainsKeyと、Addで「一致するキーを探す」という処理を2回も行っています。
 ご存知かと思いますが、たいていの場合、MapやDictionaryの「一致するキーを探す」処理が追加処理(時間)の大半を占めます(TreeMapなどで、回転を行う場合や、配列が一杯になって、コピーする場合もその限りではありませんが)。
 そのため、if ContainsKey+Addの場合は2倍近い時間がかかり、わざわざ使おうという気がしません。
 別にオーダーが変わっているわけではないので、if TryAddのコードの可読性が低いようならif ContainsKey+Addを使いますが、1行減&可読性はそんなに下がらないので後者の方が良いかなと。

 さらに改善するならnullを
static readonly Object Failed = null; (名前はKeyDuplicatedでもよい)
 に変え、
if ( dictionary.TryAdd(key, value) == Failed )
 とすれば十分、可読性は確保できると思います。

お礼日時:2013/10/09 10:25

> それでもデフォルトで、一番使われそうなadd/Addメソッドがあまり使われないようなクラスというのはい


> かがなものなのでしょうか?
そんなことないと思いますけどね。

Add()は追加を前提とする場合の処理であって、TryAdd()は追加されない場面も少なからず存在します。

例えばAdd()で例外が発生する場合、この場合は、単純にコードが不正だから成り立つ事柄です。
これについては、『追加することを前提』としたコードを書いているわけですから、
例外が発生した方がベターであることは誰でもわかるでしょう。

それに対してTryAdd()でnullが返却される場合、別に不正でもなんでもないわけです。
コードでそういう動きが必要だからそうするわけですよね?
(例えばある特定の条件化では既に事前にkey『ABC』が格納済みであり、それを何よりも優先
させることを前提で後続処理を行いたいなど)

個人的には、走行させるにも関わらず変化がないロジックを書くことは解析の手間上、好みませんが。


つまり、場面に応じてAdd()、TryAdd()を使い分けると思います。
場面を無視して常にTryAdd()を使用することは正直正しい選択とは思いません。
私ならむしろTryAdd()の方がほぼ利用しません。
(TryAdd()した結果を判定してどうのこうのしたい時くらいで、格納できなかった場合に
何もしないみたいなロジックは書かない。書いたら検証してる意味がない。)

何らかの潜在的な障害があった時、TryAdd()で書いてて、本来は値が書き換わってほしい、
もしくはそこで初めて値が入ると認識していた場合に誤動作を起こすわけですから、
そういう認識ならば、本来はAdd()で記述して例外が発生した方が正確でしょう。


だから、(2)があれば(1)は不要という結論には至りません。
    • good
    • 0
この回答へのお礼

 回答ありがとうございます。
 よくよく考えてみれば、長い間ライブラリ作りばっかりやってきたことが原因でしょうか?
 終端部分であるアプリケーションのコードで使用されることより先に、自分が次(今)作るライブラリで使用することを想定して作って(質問して)いました。
 そのため、他のクラスをDecoratorとして使うと、そのクラスでAddしたときに例外が出たときに、適切なエラーメッセージにならないことが多いので、TryAddを使うことが多いんですね。
 try-catchならAddでもいいのですが、ちょとだけ行が増えるので敬遠気味です。
 末端のアプリケーションなら、確かにAddの方が多いかもしれませんね。

お礼日時:2013/10/08 15:54

個人的には「キーの重複は『例外』なのか?」という疑問があるので, 今自分で作るなら (2) くらいかなぁ. ただ, これを全部作るとするともっとわかりやすい名前がほしい. 例えば, 「TryAdd」では (「try」とついている分だけ却って) 例外を送出するように感じるかもよ.



ちなみに C++ の map::insert は (4)+α だったりする (「+α」は「既にあったかどうか」の情報も返すところ).
    • good
    • 0
この回答へのお礼

 貴重なご意見ありがとうございます(ひょっとしたら久しぶりですかね)。

 ちょっと誤解がありそうなので、補足しておきます。
 Addは飽くまで「追加」で、Setを「関連付け(Associate)」としています。

> キーの重複は「例外」なのか
 個人的な意見ですが、「追加」という操作に関しては間違いなく例外だと思います。
 要素の個数はまったく増えませんから(イメージとしてはAddNew)。

> (「try」とついている分だけ却って)例外を送出するように感じる
 ですが、これはc#の、
 ・Dictionaryのboolean TryAdd(TKey key, out TValue)
 ・int(Int32)のboolean TryParse(String s, out int)
 のTry~をまねたものです。ArgumentNullExceptionくらいしかスローされません。

> c++のmap::insertは(4)+α
 +αにするならoutキーワードがないので、専用のクラス(TrialResult<T> { T result; bool succeeded; })みたいなのが要りますね。

> もっとわかりやすい名前がほしい
 こればかりは、何も言い逃れできませんね。もうちょい頑張ってみます。

お礼日時:2013/10/08 14:04

お探しのQ&Aが見つからない時は、教えて!gooで質問しましょう!