Hatena::ブログ(Diary)

中継地点 RSSフィード

2008-07-08

ensure句におけるreturnの問題

前回エントリーした「finally句におけるreturnの問題」についてrubyの場合はどうなるのか検証してみました。javaにおけるtry-catch-finallyは、rubyでのbegin-rescure-ensureと同じだと思っています。

def method
  puts "3: method start"
  begin
    raise Exception if true
  rescue Exception => e
    raise e
  ensure
    #return
  end
  puts "4: method end"
end

puts "1: proc start"
begin
  puts "2: proc method call"
  method
  puts "5: proc method called"
rescue Exception => e
  puts "6: proc catch:" + e
end
puts "7: proc end"

結果、

1: proc start
2: proc method call
3: method start
6: proc catch:Exception
7: proc end

呼び出し元procにおいて例外を補足しています。

今度は、上記ソースのensureの中でreturnを記述してみます。

1: proc start
2: proc method call
3: method start
5: proc method called
7: proc end

やはりjavaと同じ結果で例外がreturnによって握りつぶされるようです。

rubyの仕様ではメソッドの最終処理がreturn値となるようなので、試しにensureのreturnを適当な処理に変えてみましたが例外は補足できました。

現実的に、意図せず例外を潰したソースコードは結構あるのかも知れませんね。

2008-06-27

デフォルトコンストラクタ自動生成の謎

半年位前に経験したバグなのですが、突然あるクラスのデフォルトコンストラクタが見つかりませんという内容のExceptionが発生しました。

原因は他プロジェクトのjarに梱包された型をデフォルトコンストラクタインスタンス化していたのですが、このjar側のデグレードデフォルトコンストラクタがなくなったことでした。

詳細には、デフォルトコンストラクタを定義していないクラスに対して、引数付きコンストラクタを拡張したことで暗黙のデフォルトコンストラクタが消えたことによるデグレです。

デフォルトコンストラクタの参照が別プロジェクトだったためコンパイルエラーが起こらずに気が付かなかったようです。


Javaの仕様においてコンストラクタを1つも定義していない場合、コンパイル時に自動的に引数を持たない空のコンストラクタが作成されます。しかし、引数ありのコンストラクタを1つ以上定義した場合、空のコンストラクタは自動生成されません。

この暗黙のデフォルトコンストラクタの挙動を知っていたとしても、コンストラクタ拡張はメソッドのオーバーロード的な感覚のため、今回のようなデグレは十分に起こりえると思います。


暗黙のデフォルトコンストラクタ自動生成は可読性のためと思われますが、引数付きコンストラクタが存在する場合にデフォルトコンストラクタが自動生成されなくなる理由はなんででしょう?

インスタンス化されたオブジェクトが明確な初期化が行われたことを保障するためなんでしょうか。

明確な初期化を保障することとデグレを起こしやすい言語仕様と比べたときに微妙な気もします。


Rubyの場合も、引数ありinitialize(コンストラクタみたいなもの)を宣言するとデフォルトコンストラクタはなくなるみたいです。

ただ単にメソッドのオーバーロードはないのでデフォルトコンストラクタを上書きしているだけなのかも知れません。

2008-06-23

SAXパーサ作成時のバグ

以前作成したJavaSAX方式(ストリーム読込)のXMLパーサのトラブルメモ。

長いXMLを読むだけであればメモリパフォーマンスを考慮してSAXパーサを使用しますが、ちょっと意識していないとバグ埋め込んでしまうかも知れません。

  • サンプルコード
public class ParseTest extends DefaultHandler {
  public void startElement(String uri,
                           String localName,
                           String qName,
                           Attributes attributes) {
    //要素開始タグ時の処理
  }
  public void characters(char[] ch,
                         int offset,
                         int length) {
    String value = new String(ch, offset, length);
    //valueをBeanに設定
  }
  public void endElement(String uri,
                         String localName,
                         String qName) {
    //要素終了タグ時の処理
  }
}

SAXParserFactory.newInstance().newSAXParser().parse(
    new File("test.xml"), new ParseTest());

上記はよく見かけるSAXパーサのサンプルですが、長いXMLの場合に稀にvalueが予期せず分割されバグとなることがあります。

ContentHandler#charactersのリファレンスを見ると下記のことが書かれています。

文字データの通知を受け取ります。 
パーサは、このメソッドを呼び出して、各文字データチャンクを報告します。SAX パーサは、連続する文字データを単一のチャンクとして、またはいくつかのチャンクに分割して返します。

検証したところ、readbuffer(1024byte)を超えるストリーム(XML)の場合に、そのバッファの切れ目で一度charctersがコールされ、また続きのストリームから再度charctersがコールされるようで結果valueが分割されてしまう模様です。

上記のことから正しくはcharcters()ではvalueをスタックし、endElement時に結合させるような仕掛けを作る必要があります。

今更、一からSAXパーサを作る機会は滅多にないかもしれませんが、SAXのこの仕様はあまり認知されていないみたいです。



require 'rexml/parsers/streamparser'
require 'rexml/parsers/baseparser'
require 'rexml/streamlistener'

class MyListener
include REXML::StreamListener
  def text(text)
    p text
  end
end

source = File.read "test.xml"
listener = MyListener.new
REXML::Parsers::StreamParser.new(source, listener).parse

rexml/parsers/streamparserで同じように検証してみましたがTEXT要素読込で切れることはありませんでした。



javascriptでは?、PHPでは?、.netでは?、Flexでは?・・・・・後で余力があればやります。

2008-06-19

浮動小数の誤差

2進計算による浮動小数の誤差について意識なく平気で演算したり比較するケースをよく見かけます。

大抵はあまり問題は発生しないのですが、特殊なケースで予想と反する結果となります。

色々と試してみました。

  • Ruby(irb)の場合
irb(main):001:0> 1-0.9 == 1/10
=> false

RubyでFixnum型での浮動小数は2進数計算となるようです。

小数表現が内部的に2^nであらわされているために発生する問題です。

10進で値を保持するbigdecimalを使用するのが妥当な解決策と思われます。

irb(main):001:0> require 'bigdecimal'
irb(main):002:0> n1 = BigDecimal("1")
irb(main):003:0> n09 = BigDecimal("0.9")
irb(main):004:0> n10 = BigDecimal("10")
irb(main):005:0> n1-n09 == n1/n10
=> true
System.out.println(1-0.9==1/10);

falseが出力されます。

回避策は同じく10進数保持のjava.math.BigDecimalを使用します。

BigDecimal bd1 = new BigDecimal("1");
BigDecimal bd09 = 
  new BigDecimal("0.9")
  .setScale(2,BigDecimal.ROUND_HALF_UP);//有効桁数2で四捨五入
BigDecimal bd10 = new BigDecimal("10");
System.out.println(
  bd1.subtract(bd09).equals(bd1.divide(bd10,2,BigDecimal.ROUND_HALF_UP))
);

trueが出力されます。

alert(1-0.9==1/10);

falseが出力されます。

やはりNumber型も2進計算のようです。

javascriptでは標準では10進数保持の型は存在しないようで、

原始的に有効桁数を掛け算した後に演算を行い、有効桁数で割り算を行うのが一般的なようです。


javascript用のbigdecimalパッケージも存在はしているみたいですが、わざわざ使用するのも微妙な気がしますね。


問題は除算の場合に意識がある人は多いのですが、小数の四則演算が危険という認知は若干低いように思います。

小数誤差は昔からの一般常識でありコーダの意識の問題だと耳にすることもありますが、最近の高級言語では使用頻度からDecimal型が通常の数値型という訳にはいかないものなのかなと考えたりします。