はじめに
C#を教科書で学ぶと豊富な機能があることがわかるが、UWPやWPFで単機能のシンプルなアプリを開発しているうちは、あまり出番のないものもある。
ところが、アプリの機能が増え、複雑になってくると、出番が出てくる。一例として、abstract class
を使ってみようと考える局面を書き出してみる。
例示するアプリの状況
画像を編集するアプリで、ピクセル(画素)毎の明度と彩度の統計情報の収集と増減を司るclassを考える。当初、CPUで1ピクセルずつ順次処理する実装(直列処理)を行う。動作を確認し、一連のアルゴリズムが検証できたところで、CPUのベクトル演算を用いた高速化の実装(並列処理)を追加する。二つの実装はオプションの設定で切り替え可能にする。
- 対処1: 条件文で直列処理と並列処理を切り分ける。コードが長くなる、インデントが深くなるなど見通しが悪くなる、などの弊害が出てくる。
- 対処2: 直列処理と並列処理で別のclassにする。共通のPropertyやMethodのコードが重複する。
これらの弊害を抑えることを考えると、abstract class
の使用が思いつく。
abstract classの使用
元のclassを、abstract指定する親classと、直列処理と並列処理で異なる差分の実装を行う子classに分割する。
親class
public abstract class LightnessSaturation : BitmapByteArrayManager, INotifyPropertyChanged
{
// 直列処理と並列処理で共通のPropertyの実装
private SoftwareBitmap _EditingImage;
public SoftwareBitmap EditingImage
{
get { return _EditingImage; }
set { _EditingImage = value; }
}
...
// 直列処理と並列処理で共通のMethodの実装
public void ClearStats()
{
_LAverage = 0.0f;
_SAverage = 0.0f;
...
}
...
// 直列処理と並列処理で実装を分けるMethod
public abstract Task GatherStats();
public abstract Task IncrementLightness();
public abstract Task DecrementLightness();
...
}
元のclassにabstract
を付し、直列処理と並列処理で共通のPropertyやMethodはそのままに、実装を分けるMethodはabstract
指定で定義する。
子class
public class LSOpCpu : LightnessSaturation
{
// 親classでabstractで定義したMethodの実装
public override async Task GatherStats()
{
int npxls = 0;
if (!SrcImgInitialized)
{
// 親のabstract classで定義したPropertyを使用
SourceBitmap = EditingImage;
...
}
// CPUによる直列処理の実装
npxls = SourceNPxls;
for (int i = 0; i < npxls; i++)
{
...
}
...
}
public override async Task IncrementLightness()
{
...
}
public override async Task DecrementLightness()
{
...
}
}
子のclassは、親のabstract classを継承する。元の親classにあった直列処理のMethodの実装は、子のclassに移す。親classではMethodをabstractで定義したので、子classではoverrideを指定する。
public class LSOpSimd : LightnessSaturation
{
// 親classでabstractで定義したMethodの実装
public override async Task GatherStats()
{
int npxls = 0;
if (!SrcImgInitialized)
{
// 親のabstract classで定義したPropertyを使用
SourceBitmap = EditingImage;
...
}
// System.NumericsのVectorを用いた並列処理の実装
Vector<float> vmin = new Vector<float>();
Vector<float> vmax = new Vector<float>();
...
}
public override async Task IncrementLightness()
{
...
}
public override async Task DecrementLightness()
{
...
}
...
}
並列処理の方も同じ。親classでabstractで定義したMethodについて、並列処理での実装を記述する。
外部からのアクセス
public sealed partial class MainPage : Page
{
// 変数定義
private LightnessSaturation _LightnessSaturation;
private LSOpCpu _LSOpCpu;
private LSOpSimd _LSOpSimd;
...
// 初期化処理
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
...
_LSOpCpu = new LSOpCpu();
_LSOpSimd = new LSOpSimd();
_LightnessSaturation = _LSOpCpu; // default
...
}
// アプリで画像の明度照度編集ボタンが押されたときの処理
private void OpenLSCommand()
{
// アプリの設定情報から直列処理か並列処理を選択
NumericCalc opt = _Settings.NumericCalcParams.Items[_Settings.SelectedNumericCalcParamIdx].CalcMethod;
switch (opt)
{
case NumericCalc.CPU:
_LightnessSaturation = _LSOpCpu;
break;
case NumericCalc.SIMD:
_LightnessSaturation = _LSOpSimd;
break;
}
}
// 外部からのアクセス
private async void GatherLSStats()
{
// 親classの実装にアクセス
_LightnessSaturation.ClearStats();
// 子classの実装にアクセス
await _LightnessSaturation.GatherStats();
...
}
...
}
設定情報を元に、親class変数に子classを代入して直列処理か並列処理を選択する。親class変数はインスタンス化(new)しない。親クラス変数のMethodを呼び出すと、代入状況に応じて子classの実装が呼び出される。
おわりに
abstract class
を使うことで、コードが複雑になりすぎるのを防ぎ、個々の課題に集中してコードを書くことができるようになった。今後、さらに別の実装(本例でいえばGPUによる計算など)を追加する際の見通しもよくなった。
C#のあらゆる機能を使いこなせる状態でいることは難しいが、新機能追加の案内のあったときなど、折にふれおさらいしておくと、ちょっと詰まったときに脱出のヒントを得る手がかりになる。