私はgoto
が何かが悪いと地下室に閉じ込められていて決して永久に見られないことを常に知っていましたが、goto
を使用することは完全に意味のあるコード例に今日遭遇しました。
IPのリスト内にあるかどうかを確認し、コードを続行する必要があるIPがあります。それ以外の場合は例外をスローします。
<?php
$ip = '192.168.1.5';
$ips = [
'192.168.1.3',
'192.168.1.4',
'192.168.1.5',
];
foreach ($ips as $i) {
if ($ip === $i) {
goto allowed;
}
}
throw new Exception('Not allowed');
allowed:
...
goto
を使用しない場合は、次のような変数を使用する必要があります
$allowed = false;
foreach ($ips as $i) {
if ($ip === $i) {
$allowed = true;
break;
}
}
if (!$allowed) {
throw new Exception('Not allowed');
}
私の質問は、そのような明白でimoに関連するケースに使用された場合、goto
の何が悪いのでしょうか。
GOTO自体は差し迫った問題ではなく、人々がそれを使用して実装する傾向がある暗黙の状態マシンです。あなたのケースでは、IPアドレスが許可されたアドレスのリストにあるかどうかをチェックするコードが必要なので、
if (!contains($ips, $ip)) throw new Exception('Not allowed');
したがって、コードは条件をチェックする必要があります。このチェックを実装するアルゴリズムはここでは問題になりません。メインプログラムのメンタルスペースでは、チェックはアトミックです。それはそうあるべきである方法です。
しかし、チェックを行うコードをメインプログラムに入れると、それが失われます。変更可能な状態を明示的に導入します。
$list_contains_ip = undef; # STATE: we don't know yet
foreach ($ips as $i) {
if ($ip === $i) {
$list_contains_ip = true; # STATE: positive
break;
}
# STATE: we still don't know yet, huh?
# Well, then...
$list_contains_ip = false; # STATE: negative
}
if (!$list_contains_ip) {
throw new Exception('Not allowed');
}
どこ $list_contains_ip
が唯一の状態変数、または暗黙的に:
# STATE: unknown
foreach ($ips as $i) { # What are we checking here anyway?
if ($ip === $i) {
goto allowed; # STATE: positive
}
# STATE: unknown
}
# guess this means STATE: negative
throw new Exception('Not allowed');
allowed: # Guess we jumped over the trap door
ご覧のとおり、GOTO構文には宣言されていない状態変数があります。それ自体は問題ではありませんが、これらの状態変数は小石のようなものです。1つを運ぶのは難しくありません。それらをいっぱいに詰めたバッグを運ぶと、汗をかくことになります。コードは変わりません。来月はプライベートアドレスとパブリックアドレスを区別するよう求められます。その翌月、コードはIP範囲をサポートする必要があります。来年、誰かがあなたにIPv6アドレスをサポートするように頼みます。すぐに、コードは次のようになります。
if ($ip =~ /:/) goto IP_V6;
if ($ip =~ /\//) goto IP_RANGE;
if ($ip =~ /^10\./) goto IP_IS_PRIVATE;
foreach ($ips as $i) { ... }
IP_IS_PRIVATE:
foreach ($ip_priv as $i) { ... }
IP_V6:
foreach ($ipv6 as $i) { ... }
IP_RANGE:
# i don't even want to know how you'd implement that
ALLOWED:
# Wait, is this code even correct?
# There seems to be a bug in here.
そして、そのコードをデバッグしなければならない人は誰でもあなたとあなたの子供を呪います。
Dijkstra は次のように記述します。
Go toステートメントを無制限に使用すると、即時の結果として、プロセスの進行状況を記述するための意味のある座標セットを見つけるのが非常に難しくなります。
そしてそれがGOTOが有害であると考えられている理由です。
GOTO
には、いくつかの正当な使用例があります。たとえば、Cでのエラー処理とクリーンアップ、またはいくつかの形式の状態マシンの実装。しかし、これはこれらのケースの1つではありません。 2番目の例は、より読みやすいIMHOですが、ループを別の関数に抽出し、一致が見つかったときに戻ることで、さらに読みやすくなります。さらに良いでしょう(疑似コードでは、正確な構文はわかりません):
if (!in_array($ip, $ips)) throw new Exception('Not allowed');
では、GOTO
の何が悪いのでしょうか。構造化プログラミングでは、関数と制御構造を使用してコードを編成し、構文構造が論理構造を反映するようにします。何かが条件付きでのみ実行される場合、それは条件付きステートメントブロックに表示されます。ループで何かが実行されると、ループブロックに表示されます。 GOTO
を使用すると、任意にジャンプして構文構造を回避できるため、コードの追跡がはるかに困難になります。
もちろん、他に選択肢がない場合はGOTO
を使用しますが、関数と制御構造で同じ効果を得ることができる場合は、それをお勧めします。
他の人が言ったように、問題はgoto
自体にはありません。問題は、人々がgoto
をどのように使用するか、そしてそれがコードを理解および維持するのをどのように困難にするかです。
次のコードスニペットを想定します。
i = 4;
label: printf( "%d\n", i );
i
に出力される値は何ですか? いつ印刷されますか?関数でeverygoto label
のインスタンスを説明するまで、知ることはできません。そのラベルの単純な存在destroys簡単な検査によってコードをデバッグする機能。 1つまたは2つのブランチを持つ小さな関数の場合、それほど問題にはなりません。小さくない関数の場合...
90年代初頭にさかのぼって、3Dグラフィックディスプレイを駆動し、より高速に実行するように指示されたCコードの山が与えられました。わずか5000行のコードでしたが、allはmain
にあり、著者は約15程度のgoto
sを両方向に分岐させました。これはbadのコードでしたが、これらのgoto
sの存在により、状況はさらに悪化しました。同僚が制御の流れを混乱させるのに約2週間かかりました。さらに良いことに、これらのgoto
sはそれ自体と緊密に結合されたコードを生成するため、何かを壊すことなく変更を加えることはできませんできません。
レベル1の最適化でコンパイルを試みたところ、コンパイラーはすべての利用可能なRAM、次にすべての利用可能なスワップを使い果たし、システムをパニック状態にしました(おそらくgoto
s自体とは関係ありませんが、私はその逸話を投げるのが好きです)そこに)。
最後に、私たちはお客様に2つのオプションを提供しました-すべてを最初から書き直すか、より高速なハードウェアを購入しましょう。
彼らはより高速なハードウェアを購入しました。
goto
の使用に関するボーデの規則:
if
またはfor
またはwhile
ステートメントの本体に分岐しないでください)。goto
を使用しないでくださいgoto
isが正しい答えとなる場合がありますが、まれです(深くネストされたループから抜け出すことは、私がそれを使用する唯一の場所です)。
[〜#〜]編集[〜#〜]
最後のステートメントを拡張して、以下にgoto
のいくつかの有効な使用例を示します。次の関数があるとします。
T ***myalloc( size_t N, size_t M, size_t P )
{
size_t i, j, k;
T ***arr = malloc( sizeof *arr * N );
for ( i = 0; i < N; i ++ )
{
arr[i] = malloc( sizeof *arr[i] * M );
for ( j = 0; j < M; j++ )
{
arr[i][j] = malloc( sizeof *arr[i][j] * P );
for ( k = 0; k < P; k++ )
arr[i][j][k] = initial_value();
}
}
return arr;
}
さて、問題があります-malloc
呼び出しの1つが途中で失敗した場合はどうなりますか?ありそうなイベントとは異なり、部分的に割り当てられた配列を返したくありません。エラーで関数をベイルアウトしたくもありません。自分でクリーンアップして、部分的に割り当てられたメモリの割り当てを解除したいのです。悪い割り当てで例外をスローする言語では、それはかなり簡単です。すでに割り当てられているものを解放するために例外ハンドラを書くだけです。
Cでは、構造化された例外処理はありません。各malloc
呼び出しの戻り値を確認し、適切なアクションを実行する必要があります。
T ***myalloc( size_t N, size_t M, size_t P )
{
size_t i, j, k;
T ***arr = malloc( sizeof *arr * N );
if ( arr )
{
for ( i = 0; i < N; i ++ )
{
if ( !(arr[i] = malloc( sizeof *arr[i] * M )) )
goto cleanup_1;
for ( j = 0; j < M; j++ )
{
if ( !(arr[i][j] = malloc( sizeof *arr[i][j] * P )) )
goto cleanup_2;
for ( k = 0; k < P; k++ )
arr[i][j][k] = initial_value();
}
}
}
goto done;
cleanup_2:
// We failed while allocating arr[i][j]; clean up the previously allocated arr[i][j]
while ( j-- )
free( arr[i][j] );
free( arr[i] );
// fall through
cleanup_1:
// We failed while allocating arr[i]; free up all previously allocated arr[i][j]
while ( i-- )
{
for ( j = 0; j < M; j++ )
free( arr[i][j] );
free( arr[i] );
}
free( arr );
arr = NULL;
done:
return arr;
}
goto
を使用せずにこれを行うことはできますか?もちろん、私たちはできます-少し余分な簿記が必要です(そして、実際には、それは私が取る道です)。しかし、goto
の使用がimmediatelyではない場所を探している場合、これは悪い習慣またはデザインの兆候ですが、これは数少ないものの1つです。
return
、break
、continue
およびthrow
/catch
はすべて本質的にゴトです。これらはすべて、制御を別のコードに転送し、すべてgotoを使用して実装できます。実際、私は学校のプロジェクトで一度行ったのですが、Pascalのインストラクターは、構造上、Pascalは基本的なものよりもはるかに優れていると言っていました。
ソフトウェアエンジニアリングについて最も重要なこと(私はコーディングよりもこの用語を使用して、継続的な改善とメンテナンスを必要とする他のエンジニアと一緒にコードベースを作成するために誰かから支払われている状況を指します)はコードを読み取り可能にすることです- -何かをさせることはほとんど二次的なことです。あなたのコードは一度だけ書かれますが、ほとんどの場合、人々は何日も何週間も再訪/再学習し、それを改善して修正します-そして、彼ら(またはあなた)がゼロから始めて、覚えて/考え出す必要があるたびにあなたのコード。
何年にもわたって言語に追加されてきた機能のほとんどは、ソフトウェアの保守性を高めるためのものであり、作成が容易ではありません(一部の言語はその方向に進んでいますが、長期的な問題を引き起こすことがよくあります...)。
同様のフロー制御ステートメントと比較して、GOTOはほぼ最高の状態で追跡することができ(推奨されるケースでは単一のgotoが使用されます)、悪用されると悪夢になり、非常に簡単に悪用されます...
したがって、スパゲッティの悪夢を数年間扱った後、私たちは「いいえ」と言っただけです。コミュニティとしてこれを受け入れるつもりはありません。少しの余裕を与えられれば、多くの人々がそれを台無しにしてしまいます。あなたはそれらを使用することができます...しかし、それが完璧な場合であっても、コミュニティの歴史を理解していないので、次の男はあなたが恐ろしいプログラマであると仮定します。
コードをよりわかりやすくするために、他にも多くの構造が開発されています:関数、オブジェクト、スコープ、カプセル化、コメント(!)...だけでなく、「DRY」(重複防止)や「YAGNI」などのより重要なパターン/プロセス(コードの過度に一般化/複雑化を減らす)-すべては本当にNEXTガイがコードを読み取るためにのみインポートします(あなたが最初にしたことのほとんどを忘れた後、おそらく誰があなたになるでしょう!)
GOTO
はツールです。善にも悪にも使用できます。
古き良き時代、FORTRANとBASICでは、ほとんどonlyツールでした。
当時のコードを見ると、GOTO
が表示された場合、それが存在する理由を理解する必要があります。これは、すぐに理解できる標準的なイディオムの一部になる可能性があります...または、本来あるべきではなかった悪夢のような制御構造の一部になる可能性があります。見ているまで分からないので、間違えやすいです。
人々はより良いものを望み、より高度な制御構造が発明されました。これらはほとんどのユースケースをカバーしており、悪いGOTO
sによってやけどを負った人々はそれらを完全に禁止したいと考えていました。
皮肉にも、まれにGOTO
はそれほど悪くありません。ラベルが表示されたら、あなたはknow何か特別なことが起こっており、対応するラベルが近くにある唯一のラベルであるため、簡単に見つけることができます。
今日に早送りします。あなたはプログラミングを教える講師です。あなたはcouldと言います。「ほとんどの場合、高度な新しい構成を使用する必要がありますが、場合によっては、単純なGOTO
の方が読みやすいことがあります。」学生はそれを理解するつもりはありません。彼らはGOTO
を乱用して、判読不能なコードを作成します。
代わりに、「GOTO
悪い。GOTO
悪。GOTO
試験に不合格」と言います。学生は理解しますそれ!
goto
を除いて、すべてのPHP(およびほとんどの言語)のフロー構文は、階層的にスコープされています。
目を細めて調べたコードを想像してみてください。
a;
foo {
b;
}
c;
foo
の制御構造(if
、while
など)に関係なく、a
、b
、c
の特定の順序のみが許可されます。
a
-b
-c
、a
-c
、またはa
-b
-b
-b
-c
を使用できます。しかしneverにb
-c
またはa
-b
-a
-c
を含めることはできます。
...goto
がない限り。
$a = 1;
first:
echo 'a';
if ($a === 1) {
echo 'b';
$a = 2;
goto first;
}
echo 'c';
goto
(特にgoto
の後方)は問題が発生する可能性があり、そのままにして階層型のブロックされたフロー構造を使用するのが最善です。
goto
sには場所がありますが、ほとんどは低水準言語のマイクロ最適化です。 IMO、PHPに適した場所はありません。
参考までに、サンプルコードは、どちらの提案よりも優れた方法で記述できます。
if(!in_array($ip, $ips, true)) {
throw new Exception('Not allowed');
}
低レベル言語では、GOTOは避けられません。しかし、高レベルではプログラムを読みにくくするであるため、(言語がサポートしている場合は)避ける必要があります。
すべては、コードを読みにくくすることになります。高水準言語はサポートされています。たとえば、アセンブラーやCなどの低水準言語よりもコードを読みやすくします。
後藤は地球温暖化を引き起こさず、第三世界の貧困を引き起こしません。 コードを読みにくくするだけです。
最近のほとんどの言語には、GOTOを不要にする制御構造があります。 Javaのようなものもありません。
実際、spaguettiコードという用語は、複雑な分岐構造によるコードの原因をたどることが困難なために発生します。
goto
ステートメント自体に問題はありません。間違っているのは、この発言を不適切に使用している人たちです。
JacquesBの発言(Cでのエラー処理)に加えて、goto
を使用して、ネストされていないループを終了します。これは、break
を使用して実行できます。この場合、break
を使用することをお勧めします。
ただし、ネストされたループのシナリオがある場合は、goto
を使用する方がよりエレガント/シンプルになります。例えば:
$allowed = false;
foreach ($ips as $i) {
foreach ($ips2 as $i2) {
if ($ip === $i && $ip === $i2) {
$allowed = true;
goto out;
}
}
}
out:
if (!$allowed) {
throw new Exception('Not allowed');
}
上記の例は、ネストされたループを必要としないため、特定の問題では意味がありません。しかし、私はあなたがそれの入れ子にされたループの部分だけを見ることを望みます。
ボーナスポイント: IPのリストが少ない場合、メソッドは問題ありません。しかし、リストが大きくなった場合は、あなたのアプローチがO(n)の漸近的な最悪の実行時複雑さを持っていることを知ってください。リストが大きくなるにつれて、O(log n)(ツリー構造など)またはO(1)(ハッシュテーブル)を実現する別の方法を使用することができます。衝突なし)。
Gotoを使用すると、より高速なコードを記述できます。
本当です。気にしないで。
後藤はアセンブリに存在します!彼らはそれをjmpと呼んでいます。
本当です。気にしないで。
後藤は問題をより簡単に解決します。
本当です。気にしないで。
Gotoを使用する規律ある開発者コードを使用すると、読みやすくなります。
本当です。しかし、私はその規律あるコーダーでした。時間の経過とともにコードがどうなるかを見てきました。 Goto
は正常に始まります。次に、コードセットを再利用することを強く求めます。かなりすぐに、プログラムの状態を調べた後でも、何が起こっているのかわからないブレークポイントに自分がいることに気づきました。 Goto
を使用すると、コードの説明が難しくなります。 while
、do while
、for
、for each
switch
、subroutines
、functions
など、if
とgoto
を使用してこの処理を行うのは頭の痛いためです。
いいえ。 goto
を見たくありません。確かに、それはバイナリで存続していますが、ソースで確認する必要はありません。実際、if
は少し不安定に見え始めています。