多重定義の罠(自動キャストに注意)

関数の多重定義ってのは、非常に便利な機能で、同じ名前でも引数の数や型が違うと別の関数として判別してくれる、っていう技です。

オブジェクト指向言語は、大抵これを利用しています。

# オブジェクト指向的には必須って訳ではないんだろうけど、引数の数が異なる度に
# 関数名を考えないと駄目ってのもアレなわけで。

# あと、C++/C# に限って言えば、コンストラクタの多重定義ができないと言語的に
# 成り立たないってのがあります。

class A
{
private:
 int _x ;
public:
 // 引数のないコンストラクタ
 A() { _x = 0; }
 // 引数のあるコンストラクタ
 A( int x ) { _x = x; }
};

ここで多重定義ができないと、A(x) ってのができません。
仕方がないので、A::CreateX( int x ) なんて云うスタティッククラスを使うような
ファクトリーパターンを使うようになる訳です。Java の場合は、Factory パターンが
主流ですが、C++のほうはコンストラクタの多重定義が多いですね。

# objective-c の場合は、このあたり中途半端(っぽく見える)なので、
# Aクラスに initWithX というメソッドを作ります。

さて、この多重定義を突き詰めていくと、出来るだけ短い名前で表現したいという欲望に駆られます。勿論一方で、elementById という名前も主流でもあるわけですが。

ひとまず、ざっと次のコードをご覧ください。

using System;

public class RGB {
 private double _r;
 private double _g;
 private double _b;
 
 public RGB() {
  _r = _g = _b = 0.0;
 }
 public RGB( double r, double g, double b ) {
  _r = r;
  _g = g;
  _b = b;
 }
 public RGB( byte r, byte g, byte b ) {
  _r = ((double)r)/255.0;
  _g = ((double)g)/255.0;
  _b = ((double)b)/255.0;
 }
 public override string ToString() {
  return string.Format("r:{0} g:{1} b:{2}", _r, _g, _b );
 }
}

public class Test {
 
 public static void Main( string[] args ) {
  Console.WriteLine("RGB check");
  
  RGB col = new RGB();
  Console.WriteLine("RGB {0}", col );
  // 実数で指定
  col = new RGB(1.0, 1.0, 0.0);
  Console.WriteLine("RGB {0}", col );
  // WindowsのRGB値(0-255)で指定
  col = new RGB(0,255,255);
  Console.WriteLine("RGB {0}", col );
  // 実数で指定したつもり
  col = new RGB(1, 1, 0);  // byte型とみなされてしまう。
  Console.WriteLine("RGB {0}", col );
  col = new RGB(1.0, 1, 0); // ひとつでもdouble型にしておけばok
  Console.WriteLine("RGB {0}", col );
 }
}

RGB値を扱う関数なのですが、Mac の場合、RGB値は1.0から0.0の実数を取るんだよね~、ってことが分かって、まあ画像解析がてら double 型に組んでいたいのです。
(実際は、float 型なのですが、キャストが面倒なのと説明の関係で double 型にしてあります)

ご存じの通り、Windows の RGB 値は 0 から 255 の整数値を取ります。ですが、これはディスプレイに表示するときには便利なのですが、色相を変えたりするときは、結局実数で扱うので、そのまま扱うと不便です。

なので、内部的には double 型で扱う。けれども、RGB 値を整数値(byte型)でも渡せるよ、というクラスを作ってみたわけです。

RGB( double r, double g, double b )
RGB( byte r, byte g, byte b )

この2つのコンストラクタ(関数でもいいけど)は多重定義を使っていて、一方は double 型、もう一方は byte 型で初期化できるものです。

多重定義ができないと、

RGBByDouble( double r, double g, double b )
RGBByByte( byte r, byte g, byte b )

な感じになるんですが、まあ、RGB コンストラクタのほうがスマートでしょう?

ですが、ここに落とし穴があるんですねぇ。
先のプログラムを実行すると、こんな感じになります。

RGB check
RGB r:0 g:0 b:0
RGB r:1 g:1 b:0
RGB r:0 g:1 b:1
RGB r:0.00392156862745098 g:0.00392156862745098 b:0 ★
RGB r:1 g:1 b:0

★の部分が、意図しなかった動きです。

// 実数で指定したつもり
col = new RGB(1, 1, 0);  // byte型とみなされてしまう。
Console.WriteLine("RGB {0}", col );

実は、上記のように即値で指定する場合、「1」や「0」は整数とみなされるために、byte 型で初期化するコンストラクタが呼び出されます。勿論、C言語を習ったときのように、

「実数を指定するときは、明示的に 1.0 としないと駄目」

ってことを忘れずにいればいいんですが。

そんな訳で

col = new RGB(1.0, 1, 0); // ひとつでもdouble型にしておけばok
Console.WriteLine("RGB {0}", col );

こんな風に、即値の場合は明示的に指定するのが吉です。
自動キャストは、データ型の小さいところから判断されるので、

byte -> int -> long -> float -> double

の順番にキャストされています。先の例では byte 型なので、見当が付きそうですが、

int型とlong型の混在なんかやると、もう駄目ですね。意図通りに動きません。
なので、そういう風な場合は、「やめましょう」ってのが筋です。
そうそう、C言語ならば typedef、C# ならば型を継承するってのもひとつの方法です。
標準で定義されている typedef unsigned long size_t ; のように、しておけば型が厳密に判断されて、先の自動キャストの範囲外になり、混乱が少なくなります。

カテゴリー: 開発 パーマリンク