C#で計算式を文字列のまま実行する方法|四則演算・Eval代替・安全な実装まで解説
はじめに
C#で計算式を扱う際、数値や演算子をコードに直接記述するだけであれば、通常の四則演算で実装できます。
C#int result = 1 + 2 * 3;
Console.WriteLine(result); // 7
一方、入力フォームや設定ファイルから受け取った文字列を計算したい場合は、単純な型変換だけでは対応できません。
C#string formula = "1+2*3";
この文字列をint.Parseやdecimal.Parseに渡しても、計算結果の7には変換されません。文字列を解析し、演算子の優先順位や括弧を考慮して評価する仕組みが必要です。
C#にはJavaScriptのevalに相当する単純な標準関数はありません。その代わり、次のような方法を利用できます。
DataTable.Computeで式を評価するNCalcなどの計算式ライブラリを利用する
Roslynの
CSharpScriptでC#コードとして評価する必要な演算だけに対応したパーサーを自作する
本記事では、それぞれの実装方法、使い分け、安全性、エラー対策をサンプルコードとともに解説します。
1. C#で計算式を扱うとは?文字列の計算式を実行したいケース
1-1. 「C# 計算式」で検索するユーザーの主な目的
「C# 計算式」で検索するユーザーの目的は、大きく分けると次の2種類です。
1つ目は、C#で四則演算や数学関数を記述する方法を知りたいケースです。
C#double area = 10.5 * 8.2;
double squareRoot = Math.Sqrt(16);
2つ目は、文字列として保存されている計算式を実行したいケースです。
C#string expression = "10.5 * 8.2";
後者では、文字列の内容を単なるデータではなく、計算式として解析する必要があります。本記事では、主に文字列の計算式を実行する方法を扱います。
1-2. 文字列のまま「1+2*3」を計算したいケース
次のコードでは、変数に格納されるのは計算結果ではなく文字列です。
C#string expression = "1+2*3";
Console.WriteLine(expression); // 1+2*3
期待する結果は、掛け算を先に処理した7です。
計算式を正しく評価するには、少なくとも以下の処理が必要になります。
数値を読み取る
+、-、*、/を演算子として認識する掛け算と割り算を先に計算する
括弧内を優先して計算する
不正な文字列をエラーとして処理する
0除算や数値の範囲超過を処理する
単純に文字列を区切って左から順番に計算すると、演算子の優先順位を正しく処理できません。
1-3. 入力フォーム・設定ファイル・CSV・Excel連携で計算式が必要になる場面
文字列の計算式が必要になる代表的な場面は次のとおりです。
ユーザーが入力フォームに料金計算式を設定する
設定ファイルに税額や手数料の計算ルールを保存する
CSVの列に記録された計算式を一括評価する
データベースに登録した業務ルールを実行する
Excelから取り込んだ式をC#側で再計算する
帳票システムで項目間の計算を行う
ゲームやシミュレーションでパラメーター計算式を変更可能にする
例えば、商品価格と数量を利用して金額を計算する式を設定ファイルに保存する場合があります。
price * quantity
この場合は四則演算だけでなく、priceやquantityのような変数を計算式に渡せる仕組みが必要です。
1-4. C#にはJavaScriptのevalのような標準機能はあるのか
C#には、JavaScriptのevalのように文字列をそのまま実行する単純な標準関数はありません。
次のようなコードは記述できません。
C#string expression = "1+2";
int result = eval(expression); // C#には存在しない
ただし、目的に応じた代替手段はあります。
簡単な計算式であればDataTable.Compute、変数や関数を使うならNCalcなどのライブラリ、C#構文を評価するならCSharpScriptが候補です。
ただし、ユーザーから受け取った文字列をコードとして実行する設計には重大なセキュリティリスクがあります。機能の多さだけで選ばず、入力元と必要な演算範囲を考慮することが重要です。
2. C#で文字列の計算式を実行する代表的な方法
2-1. DataTable.Computeを使う方法
DataTable.Computeは、System.Data.DataTableが持つ式評価機能です。短いコードで文字列の四則演算を実行できます。
C#using System.Data;
string expression = "1+2*3";
var table = new DataTable();
object result = table.Compute(expression, string.Empty);
Console.WriteLine(result); // 7
外部ライブラリを追加せずに利用できる点がメリットです。
一方で、DataTable.Computeは計算式専用に設計された汎用エンジンではありません。変数の扱い、独自関数、利用可能な構文の制御などには制約があります。
2-2. NCalcなどのライブラリを使う方法
NCalc系のライブラリは、文字列の計算式を解析して評価するための機能を提供します。
C#var expression = new Expression("price * quantity");
expression.Parameters["price"] = 1200;
expression.Parameters["quantity"] = 3;
object result = expression.Evaluate();
Console.WriteLine(result); // 3600
変数、比較演算、論理演算、条件式、関数などを扱いやすいため、計算ルールを外部設定したい場合に適しています。
パッケージやバージョンによって名前空間、クラス名、非同期APIなどが異なることがあるため、導入時には利用するパッケージのドキュメントを確認してください。
2-3. RoslynやCSharpScriptでC#コードとして評価する方法
RoslynのスクリプトAPIを使うと、文字列をC#の式として評価できます。
C#decimal result =
await Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript
.EvaluateAsync<decimal>("10m + 20m * 3m");
Console.WriteLine(result); // 70
C#の構文や型、メソッドを利用できる点が大きな特徴です。
ただし、CSharpScriptは安全な計算式専用サンドボックスではありません。外部入力をそのまま渡す用途には向いていません。
2-4. 自作パーサーで四則演算だけを安全に処理する方法
必要な機能が四則演算と括弧だけであれば、専用のパーサーを実装する方法があります。
例えば、次の構文だけを許可します。
数値
数値 + 数値
数値 - 数値
数値 * 数値
数値 / 数値
(計算式)
実装量は増えますが、利用できる文字や演算子を明確に制限できます。C#コードとして実行しないため、ファイル操作やネットワークアクセスにつながる構文を最初から排除できます。
2-5. 方法ごとの特徴・安全性・実装難易度の比較
| 方法 | 四則演算 | 変数 | 関数 | C#構文 | 実装難易度 | 外部入力への適性 |
|---|---|---|---|---|---|---|
| DataTable.Compute | ○ | △ | △ | × | 低い | 検証が必要 |
| NCalcなど | ○ | ○ | ○ | × | 低~中 | 設定次第 |
| CSharpScript | ○ | ○ | ○ | ○ | 中 | 低い |
| 自作パーサー | ○ | 拡張可能 | 拡張可能 | × | 高い | 高い |
外部入力を扱う場合は、機能が多い方法ほどよいとは限りません。許可する構文を狭くできる方法のほうが、安全性を確保しやすくなります。
3. DataTable.Computeで四則演算の計算式を実行する方法
3-1. DataTable.Computeの基本的な使い方
DataTable.Computeは、次の形式で呼び出します。
C#object result = dataTable.Compute(expression, filter);
第1引数には評価する式、第2引数にはフィルター条件を指定します。単純な計算式を評価するだけなら、第2引数には空文字列を指定します。
C#using System;
using System.Data;
string expression = "100 + 20 * 3";
var table = new DataTable();
object result = table.Compute(expression, string.Empty);
Console.WriteLine(result); // 160
Console.WriteLine(result.GetType().Name);
戻り値の型はobjectです。計算式の内容によって実際の数値型が変わる可能性があるため、必要な型へ変換して利用します。
3-2. 足し算・引き算・掛け算・割り算のサンプルコード
C#using System;
using System.Data;
var table = new DataTable();
Console.WriteLine(table.Compute("10 + 5", string.Empty)); // 15
Console.WriteLine(table.Compute("10 - 5", string.Empty)); // 5
Console.WriteLine(table.Compute("10 * 5", string.Empty)); // 50
Console.WriteLine(table.Compute("10 / 5", string.Empty)); // 2
複数の演算子を含む式では、掛け算と割り算が足し算と引き算より先に処理されます。
C#object result = table.Compute("10 + 5 * 2", string.Empty);
Console.WriteLine(result); // 20
3-3. 括弧付き計算式を処理する方法
括弧を利用すると、計算順序を変更できます。
C#using System.Data;
var table = new DataTable();
object result1 = table.Compute("10 + 5 * 2", string.Empty);
object result2 = table.Compute("(10 + 5) * 2", string.Empty);
Console.WriteLine(result1); // 20
Console.WriteLine(result2); // 30
括弧が閉じられていない場合や、演算子の位置が不正な場合は例外が発生します。
C#try
{
object result = table.Compute("(10 + 5", string.Empty);
}
catch (Exception ex)
{
Console.WriteLine($"計算式が不正です: {ex.Message}");
}
3-4. decimal・double・intへの型変換で注意する点
Computeの戻り値を直接キャストすると、実際の戻り値型が想定と異なる場合にInvalidCastExceptionが発生します。
C#object rawResult = table.Compute("10 + 20", string.Empty);
// 実際の型がdecimalでなければ失敗する可能性がある
decimal result = (decimal)rawResult;
数値型を変換する場合は、Convertを利用するほうが扱いやすくなります。
C#using System.Globalization;
object rawResult = table.Compute("10 + 20", string.Empty);
int intResult = Convert.ToInt32(rawResult, CultureInfo.InvariantCulture);
decimal decimalResult =
Convert.ToDecimal(rawResult, CultureInfo.InvariantCulture);
double doubleResult =
Convert.ToDouble(rawResult, CultureInfo.InvariantCulture);
ただし、小数をintへ変換すると丸めが発生することがあります。小数部分を切り捨てたい場合は、意図を明確にして処理します。
C#decimal value = 10.9m;
int truncated = decimal.ToInt32(decimal.Truncate(value));
金額計算では、2進浮動小数点数であるdoubleよりdecimalが適していることが一般的です。一方、科学技術計算や非常に広い範囲の値を扱う場合はdoubleが選択肢になります。
3-5. DataTable.Computeで対応できない計算式
DataTable.Computeは、C#の式をそのまま実行する機能ではありません。そのため、次のようなC#構文は実行できません。
C#"Math.Sqrt(16)"
"DateTime.Now.Year"
"new Random().Next()"
"x => x * 2"
また、業務上の変数や独自関数を柔軟に追加する用途にも向いていません。
price * quantity
CalculateTax(price)
単純な数値計算には便利ですが、式の仕様が複雑になる場合は、計算式専用ライブラリや自作パーサーを検討したほうがよいでしょう。
4. Eval代替としてライブラリを使う方法
4-1. C#でevalの代わりにライブラリを使うメリット
計算式専用ライブラリを使う主なメリットは、C#コード全体を実行せずに式だけを評価できることです。
一般的には、次のような機能を利用できます。
四則演算
括弧
変数
比較演算
論理演算
条件式
数学関数
独自関数
C#コードを直接評価する方法より、許可する機能を制御しやすい点もメリットです。
ただし、ライブラリを使えば自動的に安全になるわけではありません。式の長さ、利用可能な関数、計算量、変数の型などをアプリケーション側で制限する必要があります。
4-2. NCalcで計算式を評価する基本コード
NCalc系ライブラリでは、一般的にExpressionへ式を渡し、Evaluateを呼び出します。
C#using System;
var expression = new Expression("1 + 2 * 3");
object result = expression.Evaluate();
Console.WriteLine(result); // 7
NuGetパッケージの種類やバージョンによってAPIが異なる可能性があります。実際のプロジェクトでは、採用したパッケージに対応する名前空間と利用方法を確認してください。
戻り値はobjectになることが多いため、利用側で数値型へ変換します。
C#decimal value = Convert.ToDecimal(expression.Evaluate());
4-3. 変数を含む計算式を処理する方法
NCalcを利用する大きなメリットは、変数を計算式へ渡せることです。
C#var expression = new Expression("price * quantity");
expression.Parameters["price"] = 1200m;
expression.Parameters["quantity"] = 3;
decimal result = Convert.ToDecimal(expression.Evaluate());
Console.WriteLine(result); // 3600
税率を含める場合も、式と値を分離できます。
C#var expression =
new Expression("price * quantity * (1 + taxRate)");
expression.Parameters["price"] = 1200m;
expression.Parameters["quantity"] = 3;
expression.Parameters["taxRate"] = 0.1m;
decimal result = Convert.ToDecimal(expression.Evaluate());
Console.WriteLine(result); // 3960
計算式に値を直接埋め込むのではなく、変数として渡すことで、文字列の組み立てミスや小数点表記の問題を減らせます。
4-4. 関数・条件式・複雑な式を扱う方法
ライブラリによっては、組み込み関数や条件式を利用できます。
Max(price, minimumPrice)
If(quantity >= 10, price * 0.9, price)
独自関数を登録できるライブラリであれば、業務固有の処理も追加できます。
例えば、端数処理を行うRoundDown関数や、税額を求めるTax関数などです。
ただし、独自関数にファイル操作やデータベース更新を含めると、計算式の評価がアプリケーションの副作用につながります。計算式から呼び出せる関数は、入力値から戻り値を計算するだけの純粋な処理に限定するのが基本です。
4-5. ライブラリ選定時に見るべきポイント
計算式ライブラリを選ぶ際は、次の点を確認します。
対応している.NETのバージョン
ライセンス
更新状況
四則演算、変数、関数、条件式への対応
decimalの扱いカルチャや小数点記号の仕様
独自関数の登録方法
非同期処理への対応
式の解析結果を再利用できるか
タイムアウトやキャンセルを実装できるか
エラーメッセージから入力内容が漏れないか
本番環境では、機能だけでなく、保守性や依存関係の更新方針も重要です。
5. CSharpScriptやRoslynで計算式を実行する方法
5-1. CSharpScript.EvaluateAsyncの基本
CSharpScriptを利用するには、一般的にRoslynのスクリプト用NuGetパッケージを追加します。
Microsoft.CodeAnalysis.CSharp.Scripting
基本的な評価コードは次のとおりです。
C#using Microsoft.CodeAnalysis.CSharp.Scripting;
int result = await CSharpScript.EvaluateAsync<int>("1 + 2 * 3");
Console.WriteLine(result); // 7
EvaluateAsync<T>の型引数には、期待する戻り値型を指定します。
5-2. C#の式として計算できるサンプルコード
CSharpScriptでは、C#のリテラルや演算子を利用できます。
C#decimal price =
await CSharpScript.EvaluateAsync<decimal>(
"1200m * 3m * 1.1m"
);
double squareRoot =
await CSharpScript.EvaluateAsync<double>(
"System.Math.Sqrt(16)"
);
Console.WriteLine(price); // 3960.0
Console.WriteLine(squareRoot); // 4
decimalリテラルとして扱うには、数値の末尾にmを付けます。
C#"10m / 3m"
mを付けない場合、整数同士の割り算やdoubleとしての評価になる可能性があるため注意が必要です。
5-3. 変数や独自クラスを渡して計算する方法
グローバル変数用のクラスを作成すると、計算式からプロパティを参照できます。
C#using Microsoft.CodeAnalysis.CSharp.Scripting;
public sealed class CalculationGlobals
{
public decimal Price { get; init; }
public int Quantity { get; init; }
public decimal TaxRate { get; init; }
}
評価時にインスタンスを渡します。
C#var globals = new CalculationGlobals
{
Price = 1200m,
Quantity = 3,
TaxRate = 0.1m
};
decimal result =
await CSharpScript.EvaluateAsync<decimal>(
"Price * Quantity * (1 + TaxRate)",
globals: globals
);
Console.WriteLine(result); // 3960.0
式の中に値を文字列として埋め込むより、型付きの変数として渡したほうが安全で保守しやすくなります。
5-4. CSharpScriptを使う場合のセキュリティリスク
CSharpScriptは、単なる四則演算エンジンではありません。C#コードをコンパイルして実行するため、利用可能な参照や実行環境によっては、計算以外の処理につながる可能性があります。
想定すべきリスクには、次のようなものがあります。
ファイルの読み書き
環境変数やプロセス情報へのアクセス
リフレクション
ネットワーク通信
大量のメモリ消費
長時間処理
アプリケーション内部のオブジェクト操作
例外を利用した内部情報の取得
ScriptOptionsで参照や名前空間を減らしても、それだけで安全なサンドボックスになるわけではありません。
5-5. ユーザー入力をそのまま実行してはいけない理由
次のような実装は避けるべきです。
C#string input = Console.ReadLine() ?? string.Empty;
object result =
await CSharpScript.EvaluateAsync(input);
入力者が任意のC#構文を記述できると、アプリケーションが持つ権限でコードを実行される可能性があります。
特にWebアプリでは、サーバー内の情報漏えい、ファイル操作、サービス停止などにつながる危険があります。
管理者だけが登録できる計算式であっても、誤操作やアカウント侵害を考慮する必要があります。外部から変更可能な式には、原則として計算式専用エンジンか、許可構文を限定した自作パーサーを利用してください。
6. 四則演算だけなら自作パーサーで安全に実装する
6-1. 自作パーサーが向いているケース
自作パーサーは、次のような要件に向いています。
必要なのは四則演算だけ
括弧と小数に対応したい
利用可能な構文を厳密に限定したい
外部ライブラリへの依存を避けたい
外部入力を安全に扱いたい
エラーメッセージを独自に設計したい
一方、数学関数、日付、文字列、条件分岐、配列など、多数の機能が必要な場合は自作の負担が大きくなります。
6-2. 許可する文字・演算子を限定する考え方
四則演算だけを許可する場合、入力できる文字を次の範囲に限定できます。
0~9
+
-
*
/
(
)
.
半角スペース
ただし、文字を限定するだけでは十分ではありません。
例えば、次の文字列は許可文字だけで構成されていますが、計算式としては不正です。
1++*2
(1+2
1/()
そのため、文字の検証に加えて、式の文法を解析する必要があります。
6-3. 演算子の優先順位を処理する仕組み
一般的な四則演算では、次の順番で計算します。
括弧
掛け算と割り算
足し算と引き算
この優先順位は、式を複数の階層に分けることで処理できます。
Expression = Term { ("+" | "-") Term }
Term = Factor { ("*" | "/") Factor }
Factor = Number | "(" Expression ")"
Expressionは足し算と引き算、Termは掛け算と割り算、Factorは数値や括弧を処理します。
6-4. 括弧を含む計算式を解析する流れ
Factorで左括弧を見つけた場合は、括弧内を新しいExpressionとして解析します。
例えば、次の式を考えます。
(1 + 2) * 3
処理の流れは次のとおりです。
(を読み取る括弧内の
1 + 2を解析する)を読み取る括弧内の結果
3を返す* 3を処理する結果
9を返す
括弧の解析で同じ処理を再び呼び出す方法を、再帰下降構文解析と呼びます。
6-5. 自作実装のサンプルコード
次のクラスは、数値、四則演算、単項のプラスとマイナス、括弧、小数に対応しています。
C#using System;
using System.Globalization;
public sealed class ArithmeticParser
{
private const int MaxExpressionLength = 200;
private const int MaxParenthesisDepth = 32;
private readonly string _text;
private int _position;
private int _parenthesisDepth;
public ArithmeticParser(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentException(
"計算式を入力してください。",
nameof(text)
);
}
if (text.Length > MaxExpressionLength)
{
throw new ArgumentException(
$"計算式は{MaxExpressionLength}文字以内で入力してください。",
nameof(text)
);
}
_text = text;
}
public decimal Parse()
{
decimal result = ParseExpression();
SkipWhiteSpace();
if (_position != _text.Length)
{
throw CreateFormatException(
$"解釈できない文字「{_text[_position]}」があります。"
);
}
return result;
}
private decimal ParseExpression()
{
decimal value = ParseTerm();
while (true)
{
if (Match('+'))
{
value = checked(value + ParseTerm());
}
else if (Match('-'))
{
value = checked(value - ParseTerm());
}
else
{
return value;
}
}
}
private decimal ParseTerm()
{
decimal value = ParseFactor();
while (true)
{
if (Match('*'))
{
value = checked(value * ParseFactor());
}
else if (Match('/'))
{
decimal divisor = ParseFactor();
if (divisor == 0)
{
throw new DivideByZeroException(
"0で割ることはできません。"
);
}
value /= divisor;
}
else
{
return value;
}
}
}
private decimal ParseFactor()
{
SkipWhiteSpace();
if (Match('+'))
{
return ParseFactor();
}
if (Match('-'))
{
return checked(-ParseFactor());
}
if (Match('('))
{
_parenthesisDepth++;
if (_parenthesisDepth > MaxParenthesisDepth)
{
throw CreateFormatException(
"括弧の入れ子が深すぎます。"
);
}
try
{
decimal value = ParseExpression();
if (!Match(')'))
{
throw CreateFormatException(
"閉じ括弧がありません。"
);
}
return value;
}
finally
{
_parenthesisDepth--;
}
}
return ParseNumber();
}
private decimal ParseNumber()
{
SkipWhiteSpace();
int start = _position;
bool hasDigit = false;
while (_position < _text.Length &&
char.IsDigit(_text[_position]))
{
_position++;
hasDigit = true;
}
if (_position < _text.Length &&
_text[_position] == '.')
{
_position++;
while (_position < _text.Length &&
char.IsDigit(_text[_position]))
{
_position++;
hasDigit = true;
}
}
if (!hasDigit)
{
throw CreateFormatException(
"数値が必要な位置に不正な文字があります。"
);
}
string numberText =
_text.Substring(start, _position - start);
if (!decimal.TryParse(
numberText,
NumberStyles.AllowDecimalPoint,
CultureInfo.InvariantCulture,
out decimal value))
{
throw CreateFormatException(
$"数値「{numberText}」を解釈できません。"
);
}
return value;
}
private bool Match(char expected)
{
SkipWhiteSpace();
if (_position >= _text.Length ||
_text[_position] != expected)
{
return false;
}
_position++;
return true;
}
private void SkipWhiteSpace()
{
while (_position < _text.Length &&
char.IsWhiteSpace(_text[_position]))
{
_position++;
}
}
private FormatException CreateFormatException(string message)
{
return new FormatException(
$"{message} 位置: {_position}"
);
}
}
利用方法は次のとおりです。
C#string expression = "(1 + 2) * 3 - 4 / 2";
var parser = new ArithmeticParser(expression);
decimal result = parser.Parse();
Console.WriteLine(result); // 7
不正な式は例外として処理できます。
C#try
{
var parser = new ArithmeticParser("1 + abc");
decimal result = parser.Parse();
Console.WriteLine(result);
}
catch (FormatException ex)
{
Console.WriteLine($"計算式エラー: {ex.Message}");
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"計算エラー: {ex.Message}");
}
catch (OverflowException)
{
Console.WriteLine("計算結果が数値の範囲を超えました。");
}
6-6. 自作パーサーのメリットと限界
自作パーサーのメリットは、許可する機能をコード上で明確にできることです。
先ほどの実装では、ファイル名、メソッド名、C#のキーワードなどを解析する仕組み自体がありません。そのため、C#コードを実行する方式より攻撃対象を小さくできます。
一方、次の機能を追加する場合は実装が複雑になります。
変数
関数
累乗
比較演算
条件分岐
文字列
日付
配列
ユーザー定義関数
機能追加に伴って、構文解析、型チェック、エラー表示、テストの負担も増えます。要件が大きい場合は、実績のある計算式ライブラリを選ぶほうが現実的です。
7. 安全に計算式を実行するための実装ポイント
7-1. 外部入力の計算式をそのまま実行しない
入力フォーム、CSV、API、データベース、設定ファイルなどから受け取った式は、信頼できない入力として扱います。
管理画面から入力された式であっても、次のリスクがあります。
入力ミス
権限設定の不備
管理者アカウントの侵害
CSVや設定ファイルの改ざん
想定外に重い計算
内部情報を取得する式
外部入力は必ず検証し、必要最小限の機能だけを許可します。
7-2. 使用できる文字・演算子・関数をホワイトリスト化する
禁止するものを列挙するブラックリスト方式では、想定していない構文を見落とす可能性があります。
計算式では、使用を許可するものだけを定義するホワイトリスト方式が基本です。
四則演算だけなら、次のように検証できます。
C#using System.Text.RegularExpressions;
public static bool ContainsOnlyAllowedCharacters(string expression)
{
if (string.IsNullOrWhiteSpace(expression))
{
return false;
}
if (expression.Length > 200)
{
return false;
}
return Regex.IsMatch(
expression,
@"^[0-9+\-*/().\s]+$"
);
}
ただし、この検証は文字を確認しているだけです。式の文法や0除算までは検証できないため、パーサーによる解析と組み合わせてください。
7-3. ファイル操作・ネットワーク・リフレクションを実行させない
計算式から呼び出せる関数に、次の処理を含めないようにします。
ファイルの読み書き
HTTP通信
データベース更新
プロセス実行
環境変数の取得
リフレクション
OSコマンドの呼び出し
アプリケーション終了
スレッドの生成
計算式は、入力値を受け取って結果を返す処理に限定するのが理想です。
7-4. 無限ループや重い計算式への対策
C#スクリプトや高機能な式エンジンを使う場合、無限ループや計算量の大きい式に注意が必要です。
対策として、次のような制限を設けます。
式の最大文字数
括弧の最大深度
演算子の最大数
関数呼び出し回数
配列や文字列の最大サイズ
評価時間
同時実行数
使用メモリ
再帰の深さ
キャンセルトークンを受け取れるAPIであっても、すべての処理が即座に停止するとは限りません。信頼できないコードを確実に隔離する必要がある場合は、メインのWebプロセス内で実行せず、権限とリソースを制限した別プロセスやコンテナを検討します。
7-5. 例外処理とエラーメッセージの設計
利用者には、修正方法が分かるメッセージを返します。
計算式の15文字目に解釈できない文字があります。
閉じ括弧が不足しています。
0で割ることはできません。
計算結果が許容範囲を超えました。
使用できない関数が指定されています。
一方、スタックトレース、ファイルパス、サーバー情報、内部クラス名などは画面へ表示しないようにします。
C#try
{
decimal result =
new ArithmeticParser(input).Parse();
return Results.Ok(new { result });
}
catch (FormatException)
{
return Results.BadRequest(
new { message = "計算式の形式が正しくありません。" }
);
}
詳細な例外情報は、アクセス権を制限したサーバーログへ記録します。
7-6. Webアプリで計算式を受け取る場合の注意点
Webアプリでは、計算式の検証だけでなく、API全体の保護も必要です。
リクエストサイズを制限する
認証と認可を行う
レート制限を設定する
タイムアウトを設定する
同時実行数を制限する
結果の最大値と最小値を定める
エラー内容をそのまま返さない
入力式をログへ記録する際は機密情報を除外する
同じ式を大量に送信する攻撃を想定する
計算式評価APIは、少量のリクエストでもCPUを長時間使用する可能性があります。通常の入力フォームより厳しい制限が必要になる場合があります。
8. C#で計算式を実行する実装例
8-1. もっとも簡単な四則演算の実装例
短いコードで固定的な四則演算を評価するなら、DataTable.Computeを利用できます。
C#using System;
using System.Data;
using System.Globalization;
string expression = "1 + 2 * 3";
var table = new DataTable();
object rawResult =
table.Compute(expression, string.Empty);
decimal result =
Convert.ToDecimal(
rawResult,
CultureInfo.InvariantCulture
);
Console.WriteLine(result); // 7
信頼できる固定式や、管理された設定値を簡単に評価する用途に向いています。
8-2. ユーザー入力の計算式を検証してから実行する例
入力文字、長さ、式の文法を確認してから評価します。
C#using System;
using System.Text.RegularExpressions;
public static class SafeCalculator
{
private static readonly Regex AllowedCharacters =
new(
@"^[0-9+\-*/().\s]+$",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100)
);
public static decimal Calculate(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
throw new ArgumentException(
"計算式を入力してください。"
);
}
if (input.Length > 200)
{
throw new ArgumentException(
"計算式が長すぎます。"
);
}
if (!AllowedCharacters.IsMatch(input))
{
throw new FormatException(
"使用できない文字が含まれています。"
);
}
return new ArithmeticParser(input).Parse();
}
}
利用側では、想定される例外を分けて処理します。
C#try
{
string input = "(100 + 20) * 3";
decimal result = SafeCalculator.Calculate(input);
Console.WriteLine($"計算結果: {result}");
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
catch (FormatException ex)
{
Console.WriteLine(ex.Message);
}
catch (DivideByZeroException ex)
{
Console.WriteLine(ex.Message);
}
catch (OverflowException)
{
Console.WriteLine(
"計算結果が許容範囲を超えました。"
);
}
8-3. 変数を含む計算式を評価する例
変数を使う場合は、計算式ライブラリを利用すると実装しやすくなります。
C#var expression =
new Expression(
"price * quantity * (1 + taxRate)"
);
expression.Parameters["price"] = 1500m;
expression.Parameters["quantity"] = 4;
expression.Parameters["taxRate"] = 0.1m;
decimal result =
Convert.ToDecimal(expression.Evaluate());
Console.WriteLine(result); // 6600
式に変数を埋め込む次の方法は避けたほうが安全です。
C#string expression =
$"{price} * {quantity} * (1 + {taxRate})";
カルチャによって小数点がカンマになる、意図しない文字列が混入する、式の構造が変化するといった問題が発生する可能性があります。値はパラメーターとして渡してください。
8-4. 計算結果を画面やログに出力する例
コンソールへ表示する場合は、表示用の書式を指定できます。
C#decimal result = 1234.5m;
Console.WriteLine(
$"計算結果: {result:N2}"
);
Web画面では、計算結果を文字列ではなく数値として返すと、呼び出し側で扱いやすくなります。
C#return Results.Ok(new
{
expression = input,
result
});
ログには、成功・失敗、処理時間、式の識別情報などを記録します。ただし、計算式に顧客情報や機密値が含まれる可能性がある場合は、式全体を保存しない設計が必要です。
C#logger.LogInformation(
"計算式を評価しました。Length={Length}",
input.Length
);
8-5. 不正な計算式を安全に弾く例
次の入力は拒否すべきです。
1 + abc
1 + System.IO.File.ReadAllText(...)
1 / 0
((((((((多数の括弧
非常に長い計算式
検証と評価を1つのメソッドへまとめると、呼び出し側による検証漏れを防げます。
C#public static bool TryCalculate(
string input,
out decimal result,
out string errorMessage)
{
result = 0;
errorMessage = string.Empty;
try
{
result = SafeCalculator.Calculate(input);
return true;
}
catch (ArgumentException ex)
{
errorMessage = ex.Message;
}
catch (FormatException ex)
{
errorMessage = ex.Message;
}
catch (DivideByZeroException)
{
errorMessage = "0で割ることはできません。";
}
catch (OverflowException)
{
errorMessage =
"計算結果が許容範囲を超えました。";
}
return false;
}
利用例は次のとおりです。
C#if (TryCalculate(
"10 / 0",
out decimal result,
out string error))
{
Console.WriteLine(result);
}
else
{
Console.WriteLine(error);
}
9. 目的別に選ぶC#計算式実行方法
9-1. 簡単な四則演算だけならDataTable.Compute
次の条件であれば、DataTable.Computeは手軽な選択肢です。
式が単純である
外部ライブラリを追加したくない
変数や独自関数が不要
入力元を管理できる
短期間で実装したい
ただし、入力を無検証で渡さず、文字数と使用可能な文字を制限してください。
長期運用する重要な業務ロジックでは、DataTableの式仕様に依存することが適切かどうかも検討する必要があります。
9-2. 変数や関数も使いたいならNCalcなどのライブラリ
次の要件では、計算式専用ライブラリが適しています。
priceやquantityなどの変数を使いたい条件式を使いたい
数学関数を使いたい
独自関数を登録したい
式をデータベースや設定ファイルで管理したい
C#コード自体は実行させたくない
ライブラリの機能をすべて公開するのではなく、業務上必要な関数だけを有効にすることが重要です。
9-3. C#構文そのものを評価したいならCSharpScript
CSharpScriptは、次のような内部ツールや開発支援機能で候補になります。
開発者が記述したスクリプトを実行する
C#構文を使ったルールエンジンを構築する
管理された環境で動的な処理を行う
プロトタイプとして式を評価する
一方、不特定多数のユーザーが入力した式をWebサーバー内で直接評価する用途には適しません。
9-4. セキュリティを重視するなら自作パーサー
使用する機能を四則演算などに限定できる場合、自作パーサーは安全性を高めやすい方法です。
特に次のケースで有効です。
インターネット経由で式を受け取る
金額や数量の簡単な計算だけを行う
使用できる構文を明文化したい
コード実行機能を持たせたくない
エラー位置を利用者へ示したい
自作する場合は、正常系だけでなく異常系のテストを十分に作成してください。
9-5. 本番環境でおすすめの選び方
本番環境では、次の順番で検討すると選びやすくなります。
文字列の計算式が本当に必要か確認する
必要な演算子と関数を洗い出す
入力者と入力経路を確認する
最小限の機能で実現できる方法を選ぶ
入力制限とリソース制限を設ける
異常系テストと負荷テストを行う
単純な四則演算なら自作パーサー、変数や関数が必要なら計算式専用ライブラリが有力です。
CSharpScriptは柔軟ですが、安全性を優先する外部入力向けの第一候補にはなりません。
10. C#の計算式実行でよくあるエラーと対処法
10-1. 構文エラーになる計算式の例
次のような式は構文エラーになります。
1 + * 2
(1 + 2
1 2
1 + )
* 10
対処するには、パーサーがエラー位置を返せるようにします。
数値が必要な位置に「*」があります。位置: 4
閉じ括弧がありません。位置: 6
単に「計算に失敗しました」と表示するより、利用者が修正しやすくなります。
10-2. 0除算が発生した場合の対処
整数やdecimalの計算では、0で割るとDivideByZeroExceptionが発生します。
C#try
{
decimal value = 10m;
decimal divisor = 0m;
decimal result = value / divisor;
}
catch (DivideByZeroException)
{
Console.WriteLine(
"0で割ることはできません。"
);
}
自作パーサーでは、割り算を実行する前に除数を確認します。
C#if (divisor == 0)
{
throw new DivideByZeroException(
"0で割ることはできません。"
);
}
浮動小数点型では、演算内容によってInfinityやNaNになる場合があります。doubleを使う場合は、計算後の値も検証します。
C#if (double.IsInfinity(result) ||
double.IsNaN(result))
{
throw new ArithmeticException(
"有効な計算結果を取得できませんでした。"
);
}
10-3. 小数点やカルチャ設定によるエラー
日本語環境では一般的に小数点に.を使いますが、地域によっては,を使うことがあります。
計算式の保存形式を環境に依存させると、サーバーや実行環境を変更した際に解析結果が変わる可能性があります。
計算式の内部形式では、カルチャを固定する方法が安全です。
C#decimal.TryParse(
text,
NumberStyles.AllowDecimalPoint,
CultureInfo.InvariantCulture,
out decimal value
);
画面表示では利用者のカルチャを使い、保存と解析ではInvariantCultureを使うように役割を分けると管理しやすくなります。
10-4. 型変換で失敗する原因
よくある失敗は、objectを直接キャストすることです。
C#object raw = 10;
// rawの実際の型がintなので失敗する
decimal value = (decimal)raw;
Convert.ToDecimalを使うと、変換可能な数値型をdecimalへ変換できます。
C#decimal value = Convert.ToDecimal(raw);
ただし、文字列、DBNull、範囲外の数値、不正な戻り値では例外が発生します。ライブラリが返す型を確認し、必要に応じて型ごとに処理してください。
C#decimal result = raw switch
{
decimal value => value,
int value => value,
long value => value,
double value
when !double.IsNaN(value) &&
!double.IsInfinity(value)
=> Convert.ToDecimal(value),
_ => throw new InvalidOperationException(
"計算結果が数値ではありません。"
)
};
10-5. 想定外の文字が含まれる場合のバリデーション
全角記号や似た文字が混入するケースがあります。
1+2
1+2
1×2
1÷2
これらは、半角の1+2や1*2とは異なる文字です。
全角文字を自動変換する方法もありますが、変換ルールが曖昧だと利用者の意図と異なる式になる可能性があります。
仕様として半角文字だけを許可し、エラーを表示する方法が分かりやすいでしょう。
数字と + - * / ( ) . のみ使用できます。
全角の演算子は使用できません。
自動変換を行う場合は、変換前と変換後の式を確認できるようにしてください。
11. C#計算式に関するよくある質問
11-1. C#にeval関数はありますか?
JavaScriptのevalと同じ形式の標準関数はありません。
代替方法として、次の選択肢があります。
単純な式なら
DataTable.Compute変数や関数を使うならNCalcなどのライブラリ
C#構文を評価するなら
CSharpScript四則演算だけなら自作パーサー
安全性を考えると、必要な機能が最も少ない方法を選ぶことが重要です。
11-2. 文字列の「1+2」をintに変換できますか?
int.Parse("1+2")では変換できません。int.Parseは、"123"のような単一の数値文字列を変換する機能だからです。
計算式として評価してから、結果をintへ変換します。
C#using System.Data;
var table = new DataTable();
object rawResult =
table.Compute("1+2", string.Empty);
int result = Convert.ToInt32(rawResult);
Console.WriteLine(result); // 3
外部入力の場合は、評価前に文字と式の構造を検証してください。
11-3. DataTable.Computeは本番環境で使っても安全ですか?
DataTable.Computeは、入力文字列をC#コードとして直接実行する機能ではありません。ただし、外部入力を無条件で渡してよいという意味ではありません。
次の対策が必要です。
式の最大長を制限する
使用できる文字を限定する
許可する演算子を限定する
例外を処理する
大量リクエストを制限する
計算結果の型と範囲を確認する
想定していない式構文を拒否する
重要な業務システムでは、必要な構文だけを扱う専用パーサーや計算式ライブラリも検討してください。
11-4. 計算式に変数を使うにはどうすればいいですか?
変数に対応する計算式ライブラリを使う方法が一般的です。
C#var expression =
new Expression("price * quantity");
expression.Parameters["price"] = 500m;
expression.Parameters["quantity"] = 4;
decimal result =
Convert.ToDecimal(expression.Evaluate());
Console.WriteLine(result); // 2000
C#構文が必要で、式の作成者を完全に信頼できる環境であれば、CSharpScriptへグローバルオブジェクトを渡す方法もあります。
外部ユーザーが入力する式では、利用できる変数名をホワイトリスト化してください。
11-5. Excelのような計算式をC#で実行できますか?
四則演算や一部の関数だけであれば、計算式ライブラリで似た仕組みを実装できます。
ただし、Excelの数式には次のような独自仕様があります。
セル参照
シート参照
範囲参照
Excel固有の関数
日付シリアル値
エラー値
配列数式
動的配列
循環参照
再計算ルール
NCalcなどの一般的な式ライブラリが、Excelの数式をそのまま完全互換で評価できるわけではありません。
Excelファイルの数式を再計算したい場合は、利用するExcelライブラリの数式エンジン対応状況を確認してください。すべてのExcel関数に対応しているとは限らないため、対象の関数を事前にテストする必要があります。
まとめ
C#で文字列の計算式を実行する方法には、DataTable.Compute、NCalcなどのライブラリ、CSharpScript、自作パーサーがあります。
簡単な四則演算を短いコードで処理するなら、DataTable.Computeが手軽です。変数や関数を利用する場合は、計算式専用ライブラリが適しています。C#の構文自体を評価する必要がある場合はCSharpScriptを利用できますが、信頼できない入力をそのまま実行してはいけません。
外部入力を扱い、必要な機能が四則演算と括弧だけであれば、許可する構文を限定した自作パーサーが有力です。
どの方法を選ぶ場合でも、次の対策が欠かせません。
入力文字と構文をホワイトリストで制限する
計算式の長さと括弧の深さを制限する
0除算、型変換、オーバーフローを処理する
計算時間や同時実行数を制限する
ファイル操作やネットワークアクセスを許可しない
内部情報をエラーメッセージへ含めない
計算式の実行方法は、機能の多さではなく、必要な演算範囲と入力元の信頼性を基準に選ぶことが重要です。

