WisdomSoft - for your serial experiences.

単純反復作業のコーディング効率化の問題

単純なロジックの繰り返しや、既存コードの移し替えなど、コーディング作業で発生する反復作業を体験し、どうすれば効率化できるかを考えます。次の問題「XNA Framework の Microsoft.Xna.Framework.Color 構造体から、Color 型の値を返す static プロパティのいずれかを乱数で選択し、画面を選択した色で塗りつぶしなさい。同時に、画面上に色の名前(プロパティ名)をテキストで表示しなさい。以上の処理を 1 秒ごとに繰り返し実行しなさい。」をどのように実装するのが効率的でしょうか

単純なコードの書き換え作業

プログラミングの世界では、しばしば大量のコードの移し替えのような、単純で反復的なコーディング作業が発生することがあります。以前、ゲームプログラミングが専攻の学生に次のような課題を出し、彼らがどのようにコードを記述するか、その過程を観測しました。

問題:XNA Framework の Microsoft.Xna.Framework.Color 構造体から、Color 型の値を返す static プロパティのいずれかを乱数で選択し、画面を選択した色で塗りつぶしなさい。同時に、画面上に色の名前(プロパティ名)をテキストで表示しなさい。以上の処理を 1 秒ごとに繰り返し実行しなさい。

Color 構造体には Color.Red や Color.Black など、静的なプロパティで定義済みの色を提供しています。この中から乱数を用いて適当に 1 色を選択して画面を塗りつぶし、同時に選択したプロパティの名前をテキストで表示するというものです。上の問題を適切に実装すると、以下のようなプログラムになります。

動画1 実装例

皆さんは、このプログラムをどのように実装しますか?もし XNA Framework の経験がなければ WPF を用いて System.Windows.Media.Colors クラスのプロパティのいずれかを選択して描画する形でもかまいません。問題の意図する部分は同じです。

ロジックとして複雑なことは何もありません。この問題を見れば、単純に色と色名の辞書なりテーブルなりを作成して、乱数から適当な要素を選択して画面に表示すればよいだけと考えるでしょう。最初は簡単な問題のように感じますが、開始数分で大きな問題があることに気が付きます。

Color 構造体の static プロパティで定義済みの色は 141 種類あり、ここから乱数で選択しなければなりません。Color 構造体の ToString() メソッドは ARGB の各値の文字列表現を返すだけで、色名を返すことはありません。

つまり、乱数と対応付ける Color 構造体のプロパティと、プロパティ名の文字列を個別にコードに打ち込まなければなりません。これが、本稿の題材である単純な反復作業のコーディングとなるわけです。現場でも、こうした泥臭いコーディングが必要になることがあります。この問題は、ロジックの問題ではなく、単純な反復作業をいかにして効率的に行うかを問うています。

まず、馬鹿正直にコピー&ペーストの作業で書いた場合はコード1のようになるでしょう。

コード1
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Reflection;

namespace SampleGame
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        SpriteFont spriteFont;

        private string[] colorNames;
        private Color[] colors;
        private int selectedIndex;
        private Random random;
        private double elapsedTime;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            random = new Random();
        }

        protected override void Initialize()
        {
            colorNames = new string[]
            {
                    "Transparent",
                    "AliceBlue",
                    "AntiqueWhite",
                    "Aqua",
                    "Aquamarine",
                    "Azure",
                    "Beige",
                    "Bisque",
                    "Black",
                    "BlanchedAlmond",
                    "Blue",
                    "BlueViolet",
                    "Brown",
                    "BurlyWood",
                    "CadetBlue",
                    "Chartreuse",
                    "Chocolate",
                    "Coral",
                    "CornflowerBlue",
                    "Cornsilk",
                    "Crimson",
                    "Cyan",
                    "DarkBlue",
                    "DarkCyan",
                    "DarkGoldenrod",
                    "DarkGray",
                    "DarkGreen",
                    "DarkKhaki",
                    "DarkMagenta",
                    "DarkOliveGreen",
                    "DarkOrange",
                    "DarkOrchid",
                    "DarkRed",
                    "DarkSalmon",
                    "DarkSeaGreen",
                    "DarkSlateBlue",
                    "DarkSlateGray",
                    "DarkTurquoise",
                    "DarkViolet",
                    "DeepPink",
                    "DeepSkyBlue",
                    "DimGray",
                    "DodgerBlue",
                    "Firebrick",
                    "FloralWhite",
                    "ForestGreen",
                    "Fuchsia",
                    "Gainsboro",
                    "GhostWhite",
                    "Gold",
                    "Goldenrod",
                    "Gray",
                    "Green",
                    "GreenYellow",
                    "Honeydew",
                    "HotPink",
                    "IndianRed",
                    "Indigo",
                    "Ivory",
                    "Khaki",
                    "Lavender",
                    "LavenderBlush",
                    "LawnGreen",
                    "LemonChiffon",
                    "LightBlue",
                    "LightCoral",
                    "LightCyan",
                    "LightGoldenrodYellow",
                    "LightGreen",
                    "LightGray",
                    "LightPink",
                    "LightSalmon",
                    "LightSeaGreen",
                    "LightSkyBlue",
                    "LightSlateGray",
                    "LightSteelBlue",
                    "LightYellow",
                    "Lime",
                    "LimeGreen",
                    "Linen",
                    "Magenta",
                    "Maroon",
                    "MediumAquamarine",
                    "MediumBlue",
                    "MediumOrchid",
                    "MediumPurple",
                    "MediumSeaGreen",
                    "MediumSlateBlue",
                    "MediumSpringGreen",
                    "MediumTurquoise",
                    "MediumVioletRed",
                    "MidnightBlue",
                    "MintCream",
                    "MistyRose",
                    "Moccasin",
                    "NavajoWhite",
                    "Navy",
                    "OldLace",
                    "Olive",
                    "OliveDrab",
                    "Orange",
                    "OrangeRed",
                    "Orchid",
                    "PaleGoldenrod",
                    "PaleGreen",
                    "PaleTurquoise",
                    "PaleVioletRed",
                    "PapayaWhip",
                    "PeachPuff",
                    "Peru",
                    "Pink",
                    "Plum",
                    "PowderBlue",
                    "Purple",
                    "Red",
                    "RosyBrown",
                    "RoyalBlue",
                    "SaddleBrown",
                    "Salmon",
                    "SandyBrown",
                    "SeaGreen",
                    "SeaShell",
                    "Sienna",
                    "Silver",
                    "SkyBlue",
                    "SlateBlue",
                    "SlateGray",
                    "Snow",
                    "SpringGreen",
                    "SteelBlue",
                    "Tan",
                    "Teal",
                    "Thistle",
                    "Tomato",
                    "Turquoise",
                    "Violet",
                    "Wheat",
                    "White",
                    "WhiteSmoke",
                    "Yellow",
                    "YellowGreen",
            };
            colors = new Color[]
            {
                    Color.Transparent,
                    Color.AliceBlue,
                    Color.AntiqueWhite,
                    Color.Aqua,
                    Color.Aquamarine,
                    Color.Azure,
                    Color.Beige,
                    Color.Bisque,
                    Color.Black,
                    Color.BlanchedAlmond,
                    Color.Blue,
                    Color.BlueViolet,
                    Color.Brown,
                    Color.BurlyWood,
                    Color.CadetBlue,
                    Color.Chartreuse,
                    Color.Chocolate,
                    Color.Coral,
                    Color.CornflowerBlue,
                    Color.Cornsilk,
                    Color.Crimson,
                    Color.Cyan,
                    Color.DarkBlue,
                    Color.DarkCyan,
                    Color.DarkGoldenrod,
                    Color.DarkGray,
                    Color.DarkGreen,
                    Color.DarkKhaki,
                    Color.DarkMagenta,
                    Color.DarkOliveGreen,
                    Color.DarkOrange,
                    Color.DarkOrchid,
                    Color.DarkRed,
                    Color.DarkSalmon,
                    Color.DarkSeaGreen,
                    Color.DarkSlateBlue,
                    Color.DarkSlateGray,
                    Color.DarkTurquoise,
                    Color.DarkViolet,
                    Color.DeepPink,
                    Color.DeepSkyBlue,
                    Color.DimGray,
                    Color.DodgerBlue,
                    Color.Firebrick,
                    Color.FloralWhite,
                    Color.ForestGreen,
                    Color.Fuchsia,
                    Color.Gainsboro,
                    Color.GhostWhite,
                    Color.Gold,
                    Color.Goldenrod,
                    Color.Gray,
                    Color.Green,
                    Color.GreenYellow,
                    Color.Honeydew,
                    Color.HotPink,
                    Color.IndianRed,
                    Color.Indigo,
                    Color.Ivory,
                    Color.Khaki,
                    Color.Lavender,
                    Color.LavenderBlush,
                    Color.LawnGreen,
                    Color.LemonChiffon,
                    Color.LightBlue,
                    Color.LightCoral,
                    Color.LightCyan,
                    Color.LightGoldenrodYellow,
                    Color.LightGreen,
                    Color.LightGray,
                    Color.LightPink,
                    Color.LightSalmon,
                    Color.LightSeaGreen,
                    Color.LightSkyBlue,
                    Color.LightSlateGray,
                    Color.LightSteelBlue,
                    Color.LightYellow,
                    Color.Lime,
                    Color.LimeGreen,
                    Color.Linen,
                    Color.Magenta,
                    Color.Maroon,
                    Color.MediumAquamarine,
                    Color.MediumBlue,
                    Color.MediumOrchid,
                    Color.MediumPurple,
                    Color.MediumSeaGreen,
                    Color.MediumSlateBlue,
                    Color.MediumSpringGreen,
                    Color.MediumTurquoise,
                    Color.MediumVioletRed,
                    Color.MidnightBlue,
                    Color.MintCream,
                    Color.MistyRose,
                    Color.Moccasin,
                    Color.NavajoWhite,
                    Color.Navy,
                    Color.OldLace,
                    Color.Olive,
                    Color.OliveDrab,
                    Color.Orange,
                    Color.OrangeRed,
                    Color.Orchid,
                    Color.PaleGoldenrod,
                    Color.PaleGreen,
                    Color.PaleTurquoise,
                    Color.PaleVioletRed,
                    Color.PapayaWhip,
                    Color.PeachPuff,
                    Color.Peru,
                    Color.Pink,
                    Color.Plum,
                    Color.PowderBlue,
                    Color.Purple,
                    Color.Red,
                    Color.RosyBrown,
                    Color.RoyalBlue,
                    Color.SaddleBrown,
                    Color.Salmon,
                    Color.SandyBrown,
                    Color.SeaGreen,
                    Color.SeaShell,
                    Color.Sienna,
                    Color.Silver,
                    Color.SkyBlue,
                    Color.SlateBlue,
                    Color.SlateGray,
                    Color.Snow,
                    Color.SpringGreen,
                    Color.SteelBlue,
                    Color.Tan,
                    Color.Teal,
                    Color.Thistle,
                    Color.Tomato,
                    Color.Turquoise,
                    Color.Violet,
                    Color.Wheat,
                    Color.White,
                    Color.WhiteSmoke,
                    Color.Yellow,
                    Color.YellowGreen,
            };

            selectedIndex = random.Next(colors.Length);
            base.Initialize();
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            spriteFont = Content.Load<SpriteFont>("SampleFont");
        }

        protected override void Update(GameTime gameTime)
        {
            elapsedTime += gameTime.ElapsedGameTime.Milliseconds;
            if (elapsedTime > 1000)
            {
                selectedIndex = random.Next(colors.Length);
                elapsedTime = 0;
            }
            
            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            Color color = colors[selectedIndex];
            string colorName = colorNames[selectedIndex];

            GraphicsDevice.Clear(color);

            spriteBatch.Begin();
            spriteBatch.DrawString(spriteFont, colorName, Vector2.Zero, Color.Black);
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

このコードを手入力で書きたいと思う人はいないでしょう。しかし、この方法しか知らない場合、最も確実に実装する方法でもあります。手間と時間さえかければ、多少のプログラミング経験があれば誰でも実装できる程度の簡単なコードです。同様の処理を switch 文で書くこともできますが、作業量として大きな差はありません。

このコードには効率以外にも多くの問題があります。文字列テーブルの colorNames 配列と、色テーブルの colors 配列は、それぞれの要素がインデックスで対応していますが、対応関係に間違いがないかどうかは人の目でテストしなければなりません。実際に、人間が作業した場合は文字列と色の関係が間違っていたり、色の名前が間違っているという問題が必ず発生します。

このような単純な反復作業は人間が不得意とするものであり、時間がかかるだけではなく間違いも発生しやすくなります。こうした単純作業に対しては、パターンを見つけ出して、プログラムによる変換や生成によって解決する癖をつける必要があります。そうでなければ、いつまでもコード1のようなプログラムを書いて無駄に時間を費やすことになります。

この問題は、学生にリフレクション(Reflection)の威力を理解してもらうために課題として出したものです。経験のある .NET Framework 開発者であれば、上記の問題はリフレクションを用いることで効率的に解決できると考えるでしょう。コード1を考えながら入力すると1時間以上かかりますが、リフレクションを用いてプロパティ名の一覧と、プロパティの値を取り出す方法であれば10分ほどで作業が終わります。

コード2
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace SampleGame
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        SpriteFont spriteFont;

        Dictionary<string, Color> colorDictionary;
        private string[] colorNames;
        private string colorName;
        private Random random;
        private double elapsedTime;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            colorDictionary = new Dictionary<string, Color>();
            random = new Random();
        }

        protected override void Initialize()
        {
            Type type = typeof(Color);
            var colorMembers = from p in type.GetProperties()
                               	where p.GetGetMethod().IsStatic &&
                               		p.GetGetMethod().ReturnType == typeof(Color)
                               	select p;
            foreach (PropertyInfo p in colorMembers)
            {
                MethodInfo getMethod = p.GetGetMethod();
                Color color = (Color)getMethod.Invoke(null, null);
                colorDictionary.Add(p.Name, color);
            }

            colorNames = colorDictionary.Keys.ToArray();

            base.Initialize();
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            spriteFont = Content.Load<SpriteFont>("SampleFont");
        }

        protected override void Update(GameTime gameTime)
        {
            elapsedTime += gameTime.ElapsedGameTime.Milliseconds;
            if (colorName == null || elapsedTime > 1000)
            {
                int i = random.Next(colorNames.Length);
                colorName = colorNames[i];
                elapsedTime = 0;
            }
            
            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(colorDictionary[colorName]);

            spriteBatch.Begin();
            spriteBatch.DrawString(spriteFont, colorName, Vector2.Zero, Color.Black);
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

コード2はリフレクションを用いて Color 構造体の型情報からプロパティ(PropertyInfo オブジェクト)の配列を取得し、get アクセッサが static かつ戻り値型が Color 構造体型のプロパティを抽出しています。Color 構造体の値を返す static なプロパティのコレクションを取得できれば、ここから個別にプロパティ名と、プロパティの値を取り出せます。

Initialize() メソッドの処理を解説すると、最初に Color 構造体の型を表す Type クラスのオブジェクトを typeof 演算子を用いて取得しています。Type クラスはアセンブリに含まれているクラスや構造体、インターフェイスなどの型を表し、型に含まれるメンバの情報を提供します。

次にクエリ式を用いてプロパティの抽出を行っています。クエリ式で行っている処理を個別に展開すると、以下と同じ処理を行っています。

Type type = typeof(Color);
PropertyInfo[] properties = type.GetProperties();
foreach (PropertyInfo p in properties)
{
    MethodInfo getter = p.GetGetMethod();
    if (getter.IsStatic &&  getter.ReturnType == typeof(Color))
    {
        Color color = (Color)getter.Invoke(null, null);
        colorDictionary.Add(p.Name, color);
    }
}

Type オブジェクトから、型に含まれているプロパティの一覧を取得するには GetProperties() メソッドを用います。このメソッドは、プロパティの情報を表す PropertyInfo クラスの配列を返します。この PropertyInfo クラスのインスタンスが 1 つのプロパティに対応しています。

プロパティは get と set の 2 つのアクセッサメソッドが組み合わさっているものであり、プロパティから値を取得するには get アクセッサメソッドを取得しなければなりません。プロパティの get メソッドは GetGetMethod() メソッドで取得できます。このメソッドは、メソッドの情報を表す MethodInfo クラスのオブジェクトを返します。

プロパティが静的かどうかは GetGetMethod() メソッドで取得した get メソッドの IsStatic プロパティを調べます。同様に、プロパティの戻り値型も GetGetMethod() メソッドで取得した get メソッドの ReturnType プロパティから得られます。

最後に、プロパティの値を得るために Invoke() メソッドでプロパティの get メソッドを呼び出しています。Invoke() メソッドの戻り値は常に object 型なので、目的の型にキャスト変換しています。

このような一連の実行時型解析は非常に強力で、プログラムによるコードの検出や分析に応用できます。.NET Framework や Java のような中間言語方式のプラットフォームは、プログラムの構造を実行時まで保持いるため、このような自己の構造を動的に取得できるのです。このような仕組み(API)をリフレクションと呼びます。

ただ、このやり方も完全ではありません。問題の要件を考えれば実行時型解析を行う必要はなく、問題自体はビルド時に解決可能です。ビルド時に解決可能な処理を実行時に行うのは実行コストの面では無駄であり、コード2は初期化時にプロパティの解析を行っているため、パフォーマンスでコード1に劣ります。

そこで、開発効率と実行時パフォーマンスの双方で最適化する方法として、型解析を行った結果をコードとして出力して、生成されたコードをビルドするという 3 段階の方法が考えられます。実は、上記のコード1は筆者が手入力したものではなく、リフレクションで抽出したプロパティの情報から、文字列で C# のコードを生成して出力したものです。

コード3
using System;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using Microsoft.Xna.Framework;

class Test
{
    static void Main(string[] args)
    {
        Dictionary<string, Color> colorDictionary = new Dictionary<string,Color>();
        Type type = typeof(Color);

        var colorMembers = from p in type.GetProperties()
                           where p.GetGetMethod().IsStatic select p;
        foreach (PropertyInfo p in colorMembers)
        {
            MethodInfo getMethod = p.GetGetMethod();
            Color color = (Color)getMethod.Invoke(null, null);
            colorDictionary.Add(p.Name, color);
        }

        string code = "string[] colorNames = new string[] \n{\n";
        foreach (string name in colorDictionary.Keys) 
            code += "\t\"" + name + "\",\n";
        code += "};\n";
        code += "Color[] colors = new Color[] \n{\n";
        foreach (string name in colorDictionary.Keys) 
            code += "\tColor." + name + ",\n";
        code += "};\n";

        System.Console.WriteLine(code);
    }
}
実行結果
コード3 実行結果

コード3コード2の Initialize() メソッドでやっている処理と同じ方法でプロパティを抽出し、個々のプロパティの名前を取得して C# の配列初期化子となる文字列を生成しています。これは一例であり、コード生成の方法は何でもよいのですが、とにかく事前にプログラムでコードを生成し、これを取り込むというやり方は非常に有効で、古くからある手法ですが、近年になって再び評価されている手法です。C++ のテンプレートよりも制御しやすく、出力するのは単なる文字列なので、どのような言語やプラットフォームでも通用するやり方であり、変更にも対応しやすいメリットがあります。

たとえば、本校執筆時点の主要なブラウザが対応しているスクリプト言語は JavaScript だけであり、より安全で効率的な言語を使いたくても、ブラウザが対応していません。この問題を解決するために Google が開発したプログラミング言語 Dart や、DeNA が開発したプログラミング言語 JSX などは、ブラウザ上で動作させるために JavaScript(ECMAScript)にコンパイルします。JavaScript の抱える問題を解決した効率的なプログラミング言語を、JavaScript のコードに変換することで、開発効率の問題と互換性の問題を解決しています。

最後にもう一つ。実はリフレクションやコード生成を行わなくても解決する方法があります。テキストエディタを使ったテキスト変換です。この方法は技術力とは無関係に誰でも使えるテクニックなのですが、意外と見落としがちです。上記の問題の場合 Color 構造体のメタデータからプロパティ宣言の一覧をコピーし、テキストを一括で置き換えてしまう方法があります。

図1 メタデータから宣言を複製
図1 メタデータから宣言を複製
図2 複製したテキストを置換変換
図2 複製したテキストを置換変換

どのようなやり方であれ、重要なのは単純な反復作業を避け、パターンを発見して効率的にコードを生産することを意識することです。