式(Expression)と文(Statement)
式と文の違いは知っておくといろいろ役立ちますので、ご紹介したいと思います。
プログラミング言語の構文の要素は、式(Expression)と文(Statement)に分類されます。プログラマであれば、用語としては把握していなくても、プログラミングの実践の中で、式と文を明確に区別して認識されていると思います。以降では、式と文とは何か、その違いがプログラミング言語の構文に与える影響、プログラムの書き方に与える影響を説明します。
VBで例を挙げると、文には代入文、If文、For文、While文、呼び出し文などがあり、式には条件式(比較式)、呼び出し式、加減算式、定数式などがあります。
1: Dim a As Integer 2: a = 10 ' ←代入文 3: If a = 10 Then ' ← ここのa = 10は条件式 4: Console.WriteLine("aは10です。") 5: End If
※左端の1:〜5:は行番号を示すために便宜上付加しているもので、コードの一部ではありません。
上記のコード例では、1行目のDim 〜の部分は宣言文、2行目は代入文、3〜5行目はIf文で、3行目の「a = 10」の部分は条件式で、4行目のConsole.〜の部分は、呼び出し文(戻り値を持たないメソッド呼び出しのため)です。さらに細かく言えば、2行目や3行目のaは、単純名前式で、10は定数式です。
この例から、式、文の違いがぼんやりと見えてきたでしょうか。文はそれ単独で完結する言語要素です。式はそれ単独では基本的に完結せず、文または式の一部として使用される言語要素です。また、式の最大の特徴として、値を返すという点が挙げられます(文は値を返しません)。例えば、定数式はその値そのものを返します。条件式は真偽値(True/False)を返す式です。条件式の一つとして比較式がありますが、条件式そのものはTrue/Falseを返せばなんでもよいため、単なるBooleanのTrue(定数式)もまた条件式になります。比較式は比較演算子を挟んで左辺と右辺の式を比較し、その結果をTrue/Falseで返すため、条件式になることができます。
VBとC#を式と文の観点で比較するときに代入は興味深い違いがあります。VBでは代入は文ですが、C#では代入は式です。C#の代入が式なのは、C言語の系譜を継ぐ言語だからでしょう。以下のコード例で、代入文と代入式の違いを示します。
' VBのコード Dim a As Boolean Dim b As Integer a = b = 0
// C#のコード int a; int b; a = b = 0;
上がVB、下がC#のコード例です。3行目は「a = b = 0」という形になっているのは共通しています。しかし、この行の意味はVBとC#でまったく異なります。先にC#の意味を説明すると、b = 0は代入式のため、変数bに定数0を代入します。それと同時にこの代入式は代入した値自身を返します。つまり、0を返します。その結果、
b = 0; a = 0;
と書いた場合と同じ意味になります。それに対してVBの場合、「a = ?」という形式は、代入文の形ですが、?に当たる「b = 0」の部分には、文は来ることができないルールが言語仕様で定められているため、「b = 0」は式であるとみなされ、その結果、比較式であると解釈されます。bはIntegerで宣言だけをしてあるので、暗黙的に値は0です。そのため、「b = 0」の比較式はTrueを返します。よって、a = Trueと記述した場合と同じ結果になります。
では、C#でVBの場合と同じ意味のコードを書くとどうなるかというと以下のようになります。
bool a; int b; a = b == 0;
VBでは代入演算子も比較演算子の等号もどちらも「=」ですが、C#では代入演算子は「=」、比較演算子の等号は「==」として区別されているため、このようなコードになります。
逆に言えば、C#で「a = b = 0」と記述して、aにもbにも0を代入と記述できるのは、代入演算子と比較演算子の等号とをそれ単独で区別可能な記号体系にしているからと言えます。VBでこのC#の機能を実現しようと思ったら、代入と等号を別の記号にするか、あるいは、連続的に代入するための記号を追加するといった何らかの大きな言語仕様の変更が必要です。
ちなみに、VB9.0ではIf式が追加されましたが、当記事の解説を読むとIf文とIf式の区別の意味がわかります。式と文の違いを理解していると様々な言語とその言語仕様を理解する上で役立ちます。プログラミングをする際にその違いを意識してみてはどうでしょうか。
拡張メソッドのターゲットにIList(T)とIEnumerable(T)のどちらを選ぶか?
.NET Frameworkのクラスライブラリでは、IEnumerable(T)に対して多数の拡張メソッドが用意されています。これは非常に便利です。ここで自作の拡張メソッドを作成する場合に悩むことがあります。集合に対する拡張をしたい場合、IEnumerable(T)向けに拡張メソッドを書くと柔軟性が高まることは確かですが、本質的にIEnumerable(T)のメソッドであるべきだろうか?と悩むことがあります。具体例を挙げたいと思います。私は順列を生成する以下のようなメソッドを定義しました。
/// <summary> /// 順列を生成する /// </summary> /// <param name="elements">順列の要素</param> /// <param name="selectionCount">要素から何個選択するか</param> /// <param name="permitRepeatedUse">要素から取る際に同じ要素を重複して取ることを許可するか</param> /// <returns>順列のIEnumerable(T)</returns> public static IEnumerable<T[]> CreatePermutation<T>(this IList<T> elements, int selectionCount, bool permitRepeatedUse)
第一引数は順列を生成するための要素のデータで、例えば、new string[]{"a","b","c"}などです。
順列を生成する対象データは通常、要素数が最終的には確定している配列かリストです。(IList(T)を実装していないコレクションもありますが、ここではとりあえず気にしません) 実用上はIList(T)を対象にすれば問題ないだろうと考えました。もしIEnumerable(T)を対象にしたければ、IEnumrable(T)の拡張メソッドToArray、ToListを使えばよいだけです。
逆にIEnumerable(T)を対象とする拡張メソッドとしなかった理由は以下です。
- 順列を生成する対象の集合は、生成する時点では要素数が固定であるはず
- 順列を生成するには要素数とインデックスによるアクセスが必要
- IEnumerable(T)では要素数を取るのはCount、インデックスによるアクセスはElementAtといった拡張メソッドに頼らざるを得ないが、これらのメソッドはたぶん遅いはず。
- なぜなら、IEnumerable(T)は順番に要素を挙げていくことしかできないから。
- といいつつ、ひょっとしたらCountやElementAtメソッドは内部でインスタンスの型を見てIList(T)だったらこうみたいな場合わけをしているかもしれないけれど。
つまり、IEnumerable(T)を対象とすることで得られる柔軟性よりも、IList(T)を対象とすることであるべき姿に近いのではないか、処理効率がよいのではないか、という考えが勝りました。でも、他の拡張メソッドと使い方が異なるという面で一貫性がないなぁなどと思ったりもするのですよね。(異なったものはあえて異なって見せるべきという意見もあるけれど、今回の場合、"異なった"と言えるかどうかが自信なし) みなさんはどう思いますか?
以下は順列生成メソッドのソースです。
using System; using System.Collections.Generic; namespace YKLib.Extentions { public static class IListExtentions { /// <summary> /// 順列を生成する /// </summary> /// <param name="elements">順列の要素</param> /// <param name="selectionCount">要素から何個選択するか</param> /// <param name="permitRepeatedUse">要素から取る際に同じ要素を重複して取ることを許可するか</param> /// <returns>順列のIEnumerable(T)</returns> public static IEnumerable<T[]> CreatePermutation<T>(this IList<T> elements, int selectionCount, bool permitRepeatedUse) { if (elements == null) throw new ArgumentNullException("elements"); if (selectionCount < 0) throw new ArgumentOutOfRangeException("selectionCount"); if (!permitRepeatedUse && elements.Count < selectionCount) throw new ArgumentException("重複許可のない順列では要素数以上のselectionCountは指定できません。"); return permitRepeatedUse ? CreateRepeatedPermutation<T>(elements, selectionCount) : CreatePermutation<T>(elements, selectionCount); } /// <summary> /// 重複を許可しない順列を生成する /// </summary> /// <param name="elements">順列の要素</param> /// <param name="selectionCount">要素から何個選択するか</param> /// <returns>順列のIEnumerable(T)</returns> public static IEnumerable<T[]> CreatePermutation<T>(this IList<T> elements, int selectionCount) { if (elements == null) throw new ArgumentNullException("elements"); if (selectionCount < 0) throw new ArgumentOutOfRangeException("selectionCount"); if (elements.Count < selectionCount) throw new ArgumentException("要素数以上のselectionCountは指定できません。"); int elementCount = elements.Count; int totalCountOfPermutation = (int)Math.Pow(elementCount, selectionCount); // 要素数elementCountの0〜selectionCount-1までのべき乗をあらかじめ求めておく // 後の計算の高速化のため int[] powers = new int[selectionCount]; for (int i = 0; i < selectionCount; i++) { powers[i] = (int)Math.Pow(elementCount, i); } HashSet<int> hashSet = new HashSet<int>(); T[] permutation = new T[selectionCount]; for (int i = 0; i < totalCountOfPermutation; i++) { hashSet.Clear(); for (int j = 0; j < selectionCount; j++) { int index = (i / powers[j]) % elementCount; if (!hashSet.Add(index)) { break; } permutation[selectionCount - 1 - j] = elements[index]; } if (hashSet.Count == selectionCount) { yield return permutation; } } } /// <summary> /// 重複を許可する順列を生成する /// </summary> /// <param name="elements">順列の要素</param> /// <param name="selectionCount">要素から何個選択するか</param> /// <returns>順列のIEnumerable(T)</returns> public static IEnumerable<T[]> CreateRepeatedPermutation<T>(this IList<T> elements, int selectionCount) { if (elements == null) throw new ArgumentNullException("elements"); if (selectionCount < 0) throw new ArgumentOutOfRangeException("selectionCount"); int elementCount = elements.Count; int totalCountOfPermutation = (int)Math.Pow(elementCount, selectionCount); // 要素数elementCountの0〜selectionCount-1までのべき乗をあらかじめ求めておく // 後の計算の高速化のため int[] powers = new int[selectionCount]; for (int i = 0; i < selectionCount; i++) { powers[i] = (int)Math.Pow(elementCount, i); } T[] permutation = new T[selectionCount]; for (int i = 0; i < totalCountOfPermutation; i++) { for (int j = 0; j < selectionCount; j++) { int index = (i / powers[j]) % elementCount; permutation[selectionCount - 1 - j] = elements[index]; } yield return permutation; } } } }
拡張メソッドは対象とする型の提供者以外は作成するべきではありません
非常に魅惑的な拡張メソッドですが、そのリスクについてはあまり語られていないように思えたので(百も承知だから誰も書いてないだけかもしれません) 、書いてみます。拡張メソッドとは、C#3.0、VB9.0で取り入れられた言語の機能で、詳細は下記のリンクを参照してください。
拡張メソッド (C# プログラミング ガイド)
拡張メソッド (Visual Basic)
端的に言うと、拡張メソッドだよと印を付けた静的メソッド/共有メソッドは、その静的メソッド/共有メソッドの第一引数に指定した型のインスタンスメソッドのように振舞う、という機能です。あまり意味のある使い方ではないですが、C#、VBでの使用例を以下に示します。
using System; class Program { static void Main(string[] args) { "あいう".Print(); } } static class StringExtenstions { public static void Print(this string target) { Console.WriteLine(target); } }
Imports System.Runtime.CompilerServices Module Program Sub Main() Dim s As String = "あいう" s.Print() End Sub End Module <Extension()> _ Module StringExtenstions <Extension()> _ Sub Print(ByVal target As String) Console.WriteLine(target) End Sub End Module
拡張メソッドは非常にわくわくする機能ですが、同時に危険な香りがぷんぷんします。上記のURLにはそのリスクについても記載がありますが、私なりにまとめると以下のようになります。
○あなたが拡張メソッドを作成した場合のリスク
- 同じシグネチャなら、拡張メソッドよりもインスタンスメソッドが優先されます。
- → 使用しているクラスライブラリ(例えば.NET Framework)のバージョンアップにより、拡張メソッドと同じシグネチャのインスタンスメソッドが作成されるかもしれません。(しかも動作は同じとは限りません)
○サードパーティのクラスライブラリ提供者が拡張メソッドを提供した場合のリスク
- A社の拡張メソッドとB社の拡張メソッドのシグネチャが同じだったら、どちらの拡張メソッドも使用できません。
- → 名前空間が別であれば、片方だけをインポートすることは可能。
- A社がインタフェースA向けの拡張メソッドを用意し、B社がインタフェースB向けの拡張メソッドを用意した場合、対象となる型(インタフェースAとインタフェースB)以外がまったく同じシグネチャだった場合、インタフェースAとインタフェースBを実装したクラスCに対してはこれらの拡張メソッドはどちらも使用できません。
- →IEnumerable(T)向けのメソッドXとIList(T)向けのメソッドXが定義されていたらどうでしょうか?
○拡張メソッド全体のリスク
- 上位のクラス(例えば、極端に言えばObjectクラス)に拡張メソッドを提供した場合、ありとあらゆる派生クラスにその拡張メソッドが存在することになります。派生クラスで同じシグネチャのメソッドを定義した場合、その定義したインスタンスメソッドが有効になるため動作上の問題はありませんが、間違いなく混乱のもとになります。(後、瑣末な話ですが、VisualStudioを使っている場合、常にインテリセンスにその拡張メソッドが表示されて、結構うっとおしいです。)
○拡張メソッドを提供してもよいパターン
- 自分が作成したインタフェース、列挙型などインスタンスメソッドを提供できない型に対する拡張メソッド
- 自分の汎用的でないプロジェクトに限定して使う場合で、使用しているクラスライブラリ等の今後のバージョンアップの場合に発生するリスクを見越してOKがだせる場合でしょう。
○既存の型のために拡張メソッドを用意したいと思ったら・・・
- ぜひその型を提供しているベンダーに××なメソッドが欲しい!と要望を出してみるのがよいと思います。可能であれば拡張メソッドを作成するのではなく、その型のインスタンスメソッドを作るべきであり、拡張メソッドを作成するにしても、その型の提供者でなければ、今後のバージョンアップを見据えて安全に拡張メソッドを提供することはできないからです。
いろいろ悪い使い方を考えて見ると楽しいと思いますが、あくまで実験に留めておいてくださいね。お仕事ではあまり使えないなぁというのが個人的な印象です。趣味のプログラミングではがんがん使ってます(笑) すごく便利です。
数式を解くプログラム その6(最後) - 構文解析3
id:yone-ken:20090223:p1 の続きです。さっそく、Expressionクラスから見ていきます。
class Expression : Node { public override void Evaluate(Context<Token> context) { Node left = new Term(); left.Evaluate(context); this.Value = left.Value; while (true) { Func<decimal, decimal, decimal> func = null; switch (context.Current.Kind) { case TokenKind.OpAdd: func = (d1, d2) => d1 + d2; break; case TokenKind.OpSub: func = (d1, d2) => d1 - d2; break; case TokenKind.Invalid: throw new EvaluateException("無効な文字が含まれています。", context.Current); case TokenKind.Eof: return; // 正常終了 default: return; // Expressionを終了して、処理は継続 } context.MoveNext(); Node right = new Term(); right.Evaluate(context); this.Value = func(this.Value, right.Value); } } }
ExpressionクラスはNodeクラスのEvaluateメソッドをオーバーライドしています。
加算/減算記号以外で無効なトークンの場合は評価の例外としてEvaluateExceptionを発生させます。それ以外のトークンだった場合は
class Term : Node { public override void Evaluate(Context<Token> context) { Node left = new Factor(); left.Evaluate(context); this.Value = left.Value; while (true) { Func<decimal, decimal, decimal> func = null; switch (context.Current.Kind) { case TokenKind.OpMul: func = (d1, d2) => d1 * d2; break; case TokenKind.OpDiv: func = (d1, d2) => d1 / d2; break; default: return; // Termを終了して、処理は継続 } // 計算の例外発生時のためにトークンを保持しておく。 Token snapshot = context.Current; context.MoveNext(); Node right = new Factor(); right.Evaluate(context); try { this.Value = func(this.Value, right.Value); } catch (DivideByZeroException) { throw new EvaluateException("0で除算できません。", snapshot); } } } }
基本的には
class Factor : Node { public override void Evaluate(Context<Token> context) { Node exp = null; switch (context.Current.Kind) { case TokenKind.LParen: context.MoveNext(); exp = new Expression(); exp.Evaluate(context); if (context.Current.Kind != TokenKind.RParen) { throw new EvaluateException("')'が足りません。", context.Current); } this.Value = exp.Value; break; case TokenKind.OpAdd: case TokenKind.OpSub: decimal sign = (context.Current.Kind == TokenKind.OpSub) ? -1 : 1; context.MoveNext(); exp = new Expression(); exp.Evaluate(context); this.Value = sign * exp.Value; break; case TokenKind.Number: this.Value = (decimal)context.Current.Value; break; case TokenKind.Invalid: throw new EvaluateException("無効な文字が含まれています。", context.Current); case TokenKind.Eof: throw new EvaluateException("式が途中で終わっています。", context.Current); default: throw new EvaluateException("不正な式です。", context.Current); } context.MoveNext(); } }
Factorクラスでは先頭のトークンにより処理が分岐します。左括弧'('で始まる場合は、'('
また、Evaluateメソッドの最後に「context.MoveNext()」で処理済みのトークンを読み飛ばしていますが、これは重要です。
最後にExpressionEvaluatorクラスを掲載します。今までに紹介した字句解析、構文解析を行うためのクラスを使って数式を評価するための外部向けインタフェースとなるクラスです。特に難しいことはしていませんので説明は端折ります。ざっとクラスを眺めてください。このクラスの使用例は最後に掲載します。
public class ExpressionEvaluator { public String Expression { get; private set; } private Expression exp; public decimal Value { get { if (this.LastError) { throw new InvalidOperationException("最後の評価にエラーがあります。"); } return this.exp.Value; } } public bool LastError { get; private set; } private string lastErrorMessage; public string LastErrorMessage { get { if (!this.LastError) { throw new InvalidOperationException("最後の評価にエラーはありません。"); } return this.lastErrorMessage; } private set { this.lastErrorMessage = value; } } private int lastErrorColumn; public int LastErrorColumn { get { if (!this.LastError) { throw new InvalidOperationException("最後の評価にエラーはありません。"); } return this.lastErrorColumn; } private set { this.lastErrorColumn = value; } } public ExpressionEvaluator() { this.exp = new Expression(); } public bool Evaluate(string expression) { this.Expression = expression; try { Context<Token> context = new Tokenizer(this.Expression); if (!context.MoveNext()) return false; exp.Evaluate(context); this.LastError = false; this.LastErrorColumn = 0; this.LastErrorMessage = string.Empty; return true; } catch (EvaluateException e) { this.LastError = true; this.LastErrorColumn = e.Token.Column; this.LastErrorMessage = e.Message; return false; } } }
ExpressionEvaluatorクラスのEvaluateメソッドに数式を渡すと評価を行います。数式の評価の成否は戻り値でtrue/falseで示しますので、成功していたら、Valueプロパティで結果を参照できます。評価できなかった場合は、LastErrorMessageで失敗内容をLastErrorColumnで失敗位置を取得できます。以上で、数式を解くプログラムは完成です。呼び出し部分は例えば、以下のように書けます。
class Program { static void Main(string[] args) { ExpressionEvaluator eval = new ExpressionEvaluator(); string expression = "(1+4)*3/5-2"; Console.Write(expression.PadRight(10)); if (eval.Evaluate(expression) { Console.WriteLine("= {0}", eval.Value); } else { Console.WriteLine(); Console.WriteLine(new String(' ', eval.LastErrorColumn) + "^ " + eval.LastErrorMessage); } Console.Read(); // Enter押下でコマンドプロンプトを閉じてください。 } }
数式を解くプログラムの解説はここまでです。ぼんやりとですが、このプログラムにはいろいろと課題があるなと思っています。このプログラムを元に拡張していくとその問題点も見えてくると思いますが、私自身はまだその問題点も一部しか見えていませんし、問題点に対する解決策も持っていません。徐々に研究を深めていきたいと思います。
数式を解くプログラム その5 - 構文解析2
id:yone-ken:20090220:p1 の続きです。再度、拡張BNFによる構文定義を確認します。
::= ('+' )* | ('-' )* ::= ('*' )* | ('/' )* ::= '(' ')' | '+' | '-' |
構文解析を実装するに当たり、どんなクラスが必要でしょうか?また、そのクラスにはどんなメンバが必要でしょうか?私の考えた方針は次の通りです。
この方針により、Expressionクラス、Termクラス、Factorクラスを作成することにします。また、これらが解析+計算を行うメソッドをEvaluateメソッドとし、戻り値はなしで、引数には字句解析処理を渡すことにします。計算結果はValueプロパティを用意してそこから取得することにします。(今考えるとValueプロパティは不要で、単純にEvaluateメソッドの戻り値で返すので十分な気もします。)これら3つのクラスは共通のインタフェースを持つのでこれらの抽象クラスとしてNodeクラスを用意します。
また、与えられる数式は正しい式ばかりとは限りませんし、問題なく計算できるとは限りませんので、Evaluateメソッドで解析エラー、もしくは、計算エラーがあった場合には、EvaluateExceptionを発生させることにします。
NodeクラスとEvaluateExceptionクラスを以下に掲載します。
abstract class Node { public decimal Value { get; protected set; } public abstract void Evaluate(Context<Token> context); } class EvaluateException : Exception { private Token token; public Token Token { get { return this.token; } } public EvaluateException(string messgae, Token token) : base(messgae) { this.token = token; } }
次回はExpressionクラス、Termクラス、Factorクラスと外向きのインターフェースとしてのExpressionEvaluatorクラスを紹介します。
数式を解くプログラム その4 - 構文解析1
id:yone-ken:20090219:p1 の続きです。例によって一般的な説明はwikipedia:構文解析を参照して頂くとして、「数式を解くプログラム」の場合の構文解析を具体的に説明します。そのために以前に定義した文法の抜粋を載せます。
::= ('+' )* | ('-' )* ::= ('*' )* | ('/' )* ::= '(' ')' | '+' | '-' |
上記の定義で
コンピュータに行わせる処理はその前に人間が机上で同じことができなければ実装できません。まずは机上で、さまざまな式を文法に照らし合わせてみましょう。
例1
1+2
これを解析すると以下の図1のようになります。この図では、式と構文定義を上から順に照らし合わせて、非終端記号を右辺に展開していきます。
図1 式「1+2」の解析
→ // は ではじまる。 → // は ではじまる。 → // で、今のトークンはNumber(1)のため、 である。 // 「1」は構文定義に合致した。 → + // 次のトークンはOpAddのため、その次は ではなく である。 // 「+」は構文定義に合致した。 → + // は ではじまる → + // で、今のトークンはNumber(2)のため、 である。 // 「2」は構文定義に合致した。 // 次のトークンはEofのため、構文解析は終了。
式「1+2」の字句解析では、Number、OpAdd、Number、Eofのトークンの並びが得られます。構文解析の手順は次の通りです。
- 式は
です。 は で始まります。 は で始まります。 は(、+、-のどれかで始まるか、数値です。式の最初のトークンはNumber(1)のため、文法定義 の中の が該当します。ここで の解析は終了です。 - 1つ戻って、
の解析を続けます。 の解析のうち、始めの の解析は終わっているので、次のトークンとして想定されるのは、*、/のどちらかか、そうでなければ は終了です。Numberの次のトークンはOpAddで*、/のどちらでもないので、ここで の解析は終了です。 - 1つ戻って、
の解析を続けます。 の解析のうち、始めの の解析は終わっているので、次のトークンは+、-のどちらか、そうでなければ の解析は終了です。次のトークンはOpAddのため、さらにその次のトークンは が期待されます。 は では始まります。 は(、+、-のどれかで始まるか、数値です。次のトークンはNumber(2)のため、文法定義 の中の が該当します。ここで の解析は終了です。 - 次のトークンはEofのため、これで構文解析は完了です。
式が複雑になっても、考え方は同じです。もう1例挙げます。次の例2について、自分でコンピュータになったつもりで解析してみてください。
例2
1+(2-3)*4/5
図2 式「1+(2-3)*4/5」の解析
→ → → → + → + → + ( → + ( → + ( → + ( → + ( - → + ( - → + ( - → + ( - ) → + ( - ) * → + ( - ) * → + ( - ) * / → + ( - ) * /
今回はここまでです。次回も引き続き構文解析についての説明です
数式を解くプログラム その2 - 字句解析1
id:yone-ken:20090218:1234954345 の続きです。前回記載した数式の文法を踏まえた話をしますので、必要に応じて参照してください。「数式を解く」ためにはまず字句解析という処理が必要です。
wikipedia:字句解析
例によって細かい話は上記を参照して頂くとして、字句解析というのはざっくり言うと字句に分解することです。字句は文法上で意味のある一塊です。具体的には以下の数式で見てみましょう。
20+9*(10-2)/8+1.2
このような式があったとすると、字句に分解すると
20, +, 9, *, (, 10, -, 2, ), /, 8, +, 1.2
となります(ここでの,は単なる区切りで、字句ではありません)。つまり、数値なのか演算子なのか(演算子であればどの演算子なのか)といったことを区別する作業です。これを踏まえて、これらの字句を区別するために列挙型を作成します。以下にその列挙型の定義を記載します。ちなみに、字句のことを格好よく言うとトークン(token)といいますので、トークンの種類ということでTokenKindと命名しています。さらに余談を書くとIronPythonの同様の役目を負っている列挙型もTokenKindと命名されています。
enum TokenKind
{
LParen,
RParen,
OpAdd,
OpSub,
OpMul,
OpDiv,
Number,
Invalid,
Eof
}
上から順に(, ), +, -, *, /, 数値, 無効な字句、式の終わり、を意味するものとします。丸括弧は英単語ではParenthesisとなりますが、長ったらしく、かつ、日本人はタイプミスしまくりそうなので、ここでは今時の命名ではなく短縮した名前にしています。いわずもがなですがL/RはLeft/Rightの意味です。Opと頭についているものは、演算子のグループです。Invalid(無効な字句)を用意しているのは、与えられる式が正しい文法に則ったものとは限らないため、想定外の字句を発見した場合に使います。EofはEnd Of Fileの略でファイルの終端を表す言葉のため正しい命名ではありませんが、ここではそんな細かいことは気にしないことにします(正確にはEnd Of ExpressionだとかEnd Of Lineでしょう)。この命名がいまいちだなと思ったら、リファクタリングで名称を好きに変更しましょう。Eofはとにかく式の終わりを意味します。
この列挙型とは別にトークンを表すためにTokenクラスを作成します。Tokenクラスを作る目的は、TokenKindだけでは後で困るからです。例えばTokenKind.Numberのトークンだった場合、後の処理で実際の数値が必要になります。そのため、TokenKind.Numberで数値だと判別できるだけでなく、実際の値も情報として保持しておく必要がありそうです。また、数式を評価するときに無効なトークンを見つけた場合は、後で数式のどの場所が不正なのかを判断できるようにしたいところです。というわけで、以下のようなTokenクラスを作成します。
class Token { public int Column { get; private set; } public TokenKind Kind { get; private set; } public object Value { get; private set; } public Token(TokenKind kind, int column) : this(kind, column, null) { } public Token(TokenKind kind, int column, object value) { this.Kind = kind; this.Column = column; this.Value = value; } }
TokenクラスはColumn、Kind、Valueの3つのプロパティを持ちます。Valueはトークンが数値だった場合に実際の値を保持します。また、Columnは数式を表す文字列でのトークンの開始位置を保持します。想定している数式は単一行のため、行番号は考慮しません。トークンの終了位置もあまり重要でないので考慮しません。
ここまでで字句解析を行うための準備ができました。字句解析のメイン処理は次回に説明します。