Edited at

2つの要素の有限な組み合わせをenumのネストで表現する

More than 3 years have passed since last update.


要旨


  • 組み合わせが有限な2つの要素について、enumを工夫すれば MediaInfoParam.Video.BitRateString などと表すことができる

  • 有効な組み合わせだけを持つので、入力ミス・組み合わせミスが完全になくなる

  • ネストにより、Xcodeのサジェストで探しやすくなる (はず)

  • ネストにより、直感的で作業量が少なくなる (はず)

  • 3つ以上の組み合わせもできるが、一番スマートなのは2つの場合

 具体的には、こんな感じのenumを作って活用します。

// ふたつの引数を管理するためのenum

enum MediaInfoParam {
enum General: String {
case FileFormat = "Format"
case FileSizeString = "FileSize/String"
case TotalBitRateString = "OverallBitRate/String"
case DurationString = "Duration/String"
}
enum Audio: String {
case Format = "Format"
case Duration = "Duration"
case BitRate = "BitRate"
case BitRateString = "BitRate/String"
case SamplingRate = "SamplingRate"
case SamplingRateString = "SamplingRate/String"
case Channels = "Channels"
}
}

// オーディオのビットレートを取得する
let hogeParam = hogeFunc(MediaInfoParam.Audio.BitRate)


内容

 私はいまMediaInfoというライブラリーのラッパーを書いています。

(C++で使いたいので、ObjC++経由です。)

 このライブラリーには Get という関数があります。引数は4つですが、重要なのは1つ目 StreamKind と3つ目 Parameter です。

enum stream_t {

Stream_General, Stream_Video, Stream_Audio, Stream_Text, Stream_Other, Stream_Image, Stream_Menu, Stream_Max
};

String Get(stream_t StreamKind, size_t StreamNumber, size_t Parameter, info_t InfoKind = Info_Text)

 この関数を使うこと自体は簡単です。しかし、こいつの厄介なところは、8種類のストリームに数百のパラメーターがあり、その組み合わせを間違えてもエラーメッセージが出ることは決してない (「データなし」と同じ挙動をする) という点です。


C++での対応

 このような関数ではタイプミスを避けたいですから、あらかじめ使いそうな組み合わせをリストアップし、処理を書いてクラスのどこかに隠蔽しておくほうが良さそうです。そう考えてC++で素直に実装したらこんなふうになってしまいました (なお作者はC++2週間目です)。

 まずは指定用のenumを作ってみます。

enum MediaInfoParam : int {

// General
FileFormat, FileSizeString, TotalBitRateString, DurationString,
// Video
VideoFormat, VideoMatrixCoefficients, VideoWidth, VideoWidthString, VideoHeight, VideoHeightString, VideoBitRate, VideoBitRateString, VideoFrameRate, VideoFrameRateString, VideoDisplayAspectRatio, VideoDisplayAspectRatioString, VideoPixelAspectRatio, VideoDuration, VideoScanType,
// Image
ImageFormat, ImageWidth, ImageWidthString, ImageHeight, ImageHeightString,
// Audio
AudioFormat, AudioDuration, AudioBitRate, AudioBitRateString, AudioSamplingRate, AudioSamplingRateString, AudioChannels
};

 いちいち識別子が長ったらしいですが、名前空間を使えるだけObjCより随分とシンプルです。

 次に、このenumからデータに変換する関数も作ります。

struct MediaInfoParamStruct {

stream_t kindOfStream;
string query;
};

MediaInfoParamStruct convertMediaInfoParam(const MediaInfoParam param) {
MediaInfoParamStruct s;
switch (param) {
// General
case FileFormat:
s.kindOfStream = Stream_General;
s.query = "Format";
break;
case FileSizeString:
s.kindOfStream = Stream_General;
s.query = "FileSize/String";
break;
case TotalBitRateString:
s.kindOfStream = Stream_General;
s.query = "OverallBitRate/String";
break;
case TDEnc::DurationString:
s.kindOfStream = Stream_General;
s.query = "Duration/String";
break;
// Video (略)
// Image (略)
// Audio (略)
}
return s;
}

 プログラムもへったくれもありません、完全に人力です。

 こんなものを何回もコピペして書き直すだけで非常に面倒くさいですし、コピペ由来の書き間違いも頻発します(しました)。1つのenumに沢山の選択肢をもたせているので、目的のパラメーターを探すのもかなり大変です。そして何より、見た目が美しくないのでバグ探しが苦痛そのものです。

 多数のよろしくない点がありますが、おそらく殆どの人がこのコードを見た瞬間「ウッ…」となりだいたい分かってもらえると思うので以下割愛します。

 


試行過程

 このように、1つのenumに詰めて力技でいくのはデメリットがあると分かりました。

 そこから単純に考えると、まず思いつくのが「enumを2つ用意する」という方法です。

enum StreamSelect {

case General, Video, Image, Audio
}

enum ParamSelect {
case BitRate, Width, Height
}

// オーディオのビットレートを取得する
let hogeParam = hogeFunc(.Audio, .BitRate)

 これで十分動かせますが、これだと (.Audio, .Width) というようにおよそ正当でありえない組み合わせが指定できてしまうという問題が残ります。MediaInfoのように引数の正当性を利用側で担保すべきライブラリーでは、こういう状態は好ましくないでしょう。

 そこで冒頭に書いたような、enumのネストという発想が出てきます。内側のenumにStringを持たせれば、ほとんど問題解決しそうです。

// ふたつの引数を管理するためのenum

enum MediaInfoParam {
enum General: String {
case FileFormat = "Format"
case FileSizeString = "FileSize/String"
case TotalBitRateString = "OverallBitRate/String"
case DurationString = "Duration/String"
}
enum Audio: String {
case Format = "Format"
case Duration = "Duration"
case BitRate = "BitRate"
case BitRateString = "BitRate/String"
case SamplingRate = "SamplingRate"
case SamplingRateString = "SamplingRate/String"
case Channels = "Channels"
}
}

 だいぶ見やすい、書き間違いを探したくなる外観になりましたね!

 さてこれをswitchで処理すれば作業終了です。

let param = MediaInfoParam.Audio.BitRate

switch param {
// 大きい分類で処理を分けたり… (省略した書き方)
case .General:
break
// 小さい分類でさらに処理を分けたり… (省略した書き方)
case .General.BitRate:
break
// 省略しないで書くとこんな感じです
case MediaInfoParam.General.BitRate:
break
default:
break
}

 こんな具合に書けると素晴らしいですね。

 実際に試してみます。

スクリーンショット 2015-01-06 21.37.05.png

 !?

 なんと、 MediaInfoParam.Audio.BitRateMediaInfoParam.Audio 型の値であり、それ以上でも以下でもないので、このswitch文では処理できないのです。

 省略しないで全て書けばエラー無しで処理できますが、それでは結局のところenum利用時に延々と列挙することになるので作業量が減りませんし、タイプミスへの対応策にもなりません。なによりも、データの指定に関する処理を一箇所に書けません。


swiftでの対応

 ちょっと考えてみると、ここでやろうとしているのは「よく似ているが、型違いのデータ」の扱いですから、ジェネリックを使えばよいと思い至ります (ということはC++でもスマートなやり方がありそうですが、手に余るのでパスします)。

 そこで実現したのが以下のenumです。

protocol hasMediaInfoParam {

var stream: NtyrMediaInfoStream { get }
var rawValue: String { get }
}

enum MediaInfoParam {
enum General: String, hasMediaInfoParam {
var stream: NtyrMediaInfoStream { return .General }
case FileFormat = "Format"
case FileSizeString = "FileSize/String"
case TotalBitRateString = "OverallBitRate/String"
case DurationString = "Duration/String"
}
enum Video: String, hasMediaInfoParam {
var stream: NtyrMediaInfoStream { return .Video }
case Format = "Format"
case MatrixCoefficients = "matrix_coefficients"
case Width = "Width"
case WidthString = "Width/String"
case Height = "Height"
case HeightString = "Height/String"
case BitRate = "BitRate"
case BitRateString = "BitRate/String"
case FrameRate = "FrameRate"
case FrameRateString = "FrameRate/String"
case DisplayAspectRatio = "DisplayAspectRatio"
case DisplayAspectRatioString = "DisplayAspectRatio/String"
case PixelAspectRatio = "PixelAspectRatio"
case Duration = "Duration"
case ScanType = "ScanType"
}
enum Image: String, hasMediaInfoParam {
var stream: NtyrMediaInfoStream { return .Image }
case Format = "Format"
case Width = "Width"
case WidthString = "Width/String"
case Height = "Height"
case HeightString = "Height/String"
}

enum Audio: String, hasMediaInfoParam {
var stream: NtyrMediaInfoStream { return .Audio }
case Format = "Format"
case Duration = "Duration"
case BitRate = "BitRate"
case BitRateString = "BitRate/String"
case SamplingRate = "SamplingRate"
case SamplingRateString = "SamplingRate/String"
case Channels = "Channels"
}
}

 ちょっとゴチャゴチャしていますが、最初にまるっきり人力で書いたものに比べると随分と風通しがよくなったのではないでしょうか。

 NtyrMediaInfoParam はObjC側で宣言したenumです。

typedef NS_ENUM (NSInteger, NtyrMediaInfoStream) {

NtyrMediaInfoGeneral,
NtyrMediaInfoVideo,
NtyrMediaInfoImage,
NtyrMediaInfoAudio,
};

 非常に長ったらしいですが、ObjCの作法を守っているためSwift側では自動的に処理され .Audio などと書けるのです。いやあSwiftって素晴らしいですね。

 このenumを実際に使う関数はこんな具合になります。

// ただ1つの引数を取る関数 

func getMediaInfo<T: hasMediaInfoParam>(param: T) -> String {
// 悪魔合体言語ObjC++で書かれたラッパーのインスタンス
let wrapper = NtyrMediaInfoWrapper(filePath)
// 引数から情報を取り出し、ラッパーに渡す
let stream = param.stream
let key = param.rawValue
return wrapper.getMediaInfoString(stream, key: key) // -> String
}

// 関数を使う
let str = getMediaInfoParam(MediaInfoParam.Video.BitRateString)

 有効な組み合わせだけを、十分に短く使えるようになりました。


3つ以上の要素をもたせる場合

 実際にやってはいませんが、プロトコルを書き加えればenumを三段ネストして3つの要素を持たせることもできるでしょう。

 ただその場合には2段目の情報を何回も人力で書くことになるので、あまり嬉しくありません。この方法が一番ハマるのは2要素の組み合わせ管理だと思います。


感想

 enumにプロトコルを組み合わせることで、使用時に力技を使う必要がなくなり、書き間違いの発生しうる場所をひとつにまとめることができました。これで将来いつタイプミスが発生しても即対応できます。