EXP05-J. ひとつの式の中で、オブジェクトへの書き込み後に同じオブジェクトに対して書き込みや読み込みを行わない
『Java言語仕様』§15.7「評価順序」には以下のように規定されている。[JLS 2015]
プログラミング言語Javaでは、演算子のオペランドは特定の評価順序(evaluation order)、つまり左から右へと評価されることが保証されている。
同じくJava言語仕様§15.7.3「評価には括弧や優先順位が考慮される」には以下のように規定されている。
プログラミング言語Javaの実装では、括弧によって明示的に、また演算子の評価順序によって暗黙のうちに示される順序に従って評価を行わなければならない。
式に副作用が存在する場合、これら2つの要件は予期せぬ結果をもたらす。「オペランド」の評価は、演算子の優先順位や評価順序をあらわす括弧の存在に関係なく、左から右へと行われる。しかし、「演算子」の評価は、その優先順位と括弧に従って行われる。
同一の式で、メモリへの書き込みと同時にそのメモリを読み込んではならない。また、同一の式で2度以上書込みを行ってはならない。メモリの読み書きは、代入式によって直接発生することもあれば、式中で呼ばれたメソッドの持つ副作用によって間接的に発生することもある。
違反コード(評価順序)
以下の違反コード例は、式中の副作用がどのように予期せぬ結果につながるかを示している。プログラマの意図は、閾値(threshold)に基づいたアクセス制御ロジックを実装することにある。各ユーザには点数が割り当てられ、この値が閾値より高いとアクセスが許可される。get()メソッドは、アクセスを許可するユーザにはゼロ以外の値を返し、許可しないユーザにはゼロを返すことを意図している。
*演算子は+演算子よりも評価の優先順位が高く、また部分式が括弧でくくられているため、以下の例のプログラマは誤って、最も右の部分式が最初に評価されると想定している。これはしかし、最も右の部分式number = get()によってnumberにゼロが代入されるという誤った結論につながってしまう。それゆえ、左側の部分式における条件式で、numberの値が閾値の10より小さくなり、許可されないユーザは拒否されると考える。
しかし、このプログラムは許可されていないユーザのアクセスを許してしまう。副作用を持つ部分式が左から右への評価順序に従うからである。
class BadPrecedence {
public static void main(String[] args) {
int number = 17;
int threshold = 10;
number = (number > threshold ? 0 : -2)
+ ((31 * ++number) * (number = get()));
// ...
if (number == 0) {
System.out.println("Access granted");
} else {
System.out.println("Denied access"); // number = -2
}
}
public static int get() {
int number = 0;
// 許可される場合はゼロ以外を、許可されない場合はゼロを代入する
return number;
}
}
違反コード(評価順序)
以下の違反コード例では、左から右へのオペランドの評価順序がプログラマの意図に沿うように、前述の式の順序を入れ替えている。このコードは期待通りに動作するが、1つの式で3回numberに書き込むという杜撰なコードになっている。
number = ((31 * ++number) * (number=get())) + (number > threshold ? 0 : -2);
適合コード(評価順序)
以下の適合コードでは、同じ処理を行うコードを副作用が存在しないように実装している。numberへの書き込みは1つの式につき1度しか行われない。最終的に得られる式は、部分式の評価順序を気にすることなく順序を入れ替えることができ、コードは理解しやすく保守しやすくなっている。
final int authnum = get();
number = ((31 * (number + 1)) * authnum) + (authnum > threshold ? 0 : -2);
例外
EXP05-J-EX0: インクリメント演算子++とデクリメント演算子--は、変数から値を読み込み、新しい値を変数に格納する。これらの演算子は値を読み書きするが、正しい理解が広まっているため、このルールの例外とする。この例外は、インクリメント演算子またはデクリメント演算子によって変更された値がその後に読み書きされる場合には適用されない。
EXP05-J-EX1: 条件演算子||と&&の働きについては、正しい理解が広まっている。||または&&の各オペランド(左右の部分式)内において書き込み後の読み書きが行われても、このルールには違反しない。以下のコード例について考えてみよう。
public void exampleMethod(InputStream in) {
int i;
// ''が見つかるまで文字を処理する
while ((i = in.read()) != -1 && i != '\'' &&
(i = in.read()) != -1 && i != '\'') {
// ...
}
}
条件演算子&&のオペランドそれぞれがこのルールに違反していないため、whileループの制御式はこのルールに違反しない。部分式(i = in.read()) != -1は、代入1回と副作用1つ(inからの文字の読み取り)だけしかない。
リスク評価
副作用を持つ式の評価順序を理解せずにコードを書くと、期待と異なる結果が得られる可能性がある。
|
ルール |
深刻度 |
可能性 |
自動検出 |
自動修正 |
優先度 |
レベル |
|---|---|---|---|---|---|---|
|
EXP05-J |
低 |
低 |
可 |
不可 |
P2 |
L3 |
自動検出
副作用が存在し、かつ演算子の優先度レベルが複数あるような式をすべて検出することは容易である。しかし、そのような正しさを判断することは一般に不可能である。ヒューリスティックな警告が役に立つ場合もあるだろう。
| ツール | バージョン | チェッカー | 説明 |
|---|---|---|---|
| Parasoft Jtest |
2024.2 |
CERT.EXP05.CID | Avoid using increment or decrement operators in nested expressions |
| PVS-Studio |
7.38 |
V6044 |
|
| SonarQube | 9.9 | S881 | Increment (++) and decrement (--) operators should not be used in a method call or mixed with other operators in an expression |
関連ガイドライン
|
EXP50-CPP. Do not depend on the order of evaluation for side effects |
|
| ISO/IEC TR 24772:2010 |
Side Effects and Order of Evaluation [SAM] |
参考文献
|
[JLS 2015] |
§15.7, "Evaluation Order" |
翻訳元
これは以下のページを翻訳したものです。
EXP05-J. Do not follow a write by a subsequent write or read of the same object within an expression (revision 134)
