開発者が注意すべき落とし穴
(qouteall.fun)- 開発者がよく陥る直感的ではない落とし穴を整理し、発生しやすいバグの原因を紹介する
- HTML、CSS、Unicode/テキストエンコーディング、浮動小数点、時間など、さまざまな技術で頻発する問題を扱う
- 各言語やフレームワークでの 文法や挙動の微妙な違い によって、誤解やエラーが生じうることを強調する
- 並行性、ネットワーキング、データベース などバックエンドの中核領域で、実運用環境で起こりうる落とし穴を例示して説明する
- 多様なサンプルと参考リンクを通じて、問題状況と解決法、そして予期しない挙動の改善点を案内する
HTMLとCSS
-
Flexbox/Gridにおける min-width のデフォルト値
min-widthはデフォルトでautomin-width: autoはコンテンツサイズによって決まり、flex-shrink、overflow: hidden、width: 0、max-width: 100%より優先される- 推奨:
min-width: 0を明示する
-
CSSにおける横方向と縦方向の違い
width: autoは親の空間を埋めようとし、height: autoはコンテンツに合わせられる- inline、inline-block、float 要素の
width: autoは拡張されない margin: 0 autoは水平方向の中央揃え、margin: auto 0では垂直方向の中央揃えはできない(ただしflex-direction: columnでは垂直中央が可能)- マージンの相殺は縦方向でのみ発生する
writing-mode: vertical-rlなどでレイアウト方向が変わると挙動も反転する
-
Block Formatting Context (BFC)
display: flow-rootで BFC を生成できる(そのほかoverflow: hidden/auto/scroll、display: tableなどでも可能だが副作用がある)- 縦に隣接する兄弟要素のマージンが重なったり、子要素のマージンが親の外へ漏れたりする現象は BFC で防げる
- 親が float 子要素しか含まない場合、高さが 0 に潰れる → BFC で修正可能
borderやpaddingがある場合、マージンの相殺は発生しない
-
Stacking Context
- 新しい stacking context を作る条件
transform、filter、perspective、mask、opacityなどのレンダリング属性position: fixedまたはstickyz-index指定 +absolute/relativeの位置指定z-index指定 + flexbox/grid 内部要素isolation: isolate
- 特徴
z-indexは stacking context の内部でのみ適用されるposition: absolute/fixedの座標は最も近い positioned な祖先を基準にするstickyは stacking context をまたいで動作しないoverflow: visibleでも stacking context によって切り取られるbackground-attachment: fixedは stacking context を基準に配置される
- 新しい stacking context を作る条件
-
ビューポート単位
- モバイルブラウザでは、アドレスバーやナビゲーションバーがスクロール時に画面から消えると
100vhの値が変わる - 最新の解決策:
100dvhを使う
- モバイルブラウザでは、アドレスバーやナビゲーションバーがスクロール時に画面から消えると
-
Absolute Position の基準
position: absoluteは親ではなく、最も近いrelative/absoluteまたは stacking context の祖先を基準にする
-
Blur の挙動
backdrop-filter: blurは周囲の要素を考慮しない
-
Float の無効化
- 親が
flexまたはgridの場合、子要素のfloatは効果がない
- 親が
-
パーセント単位の width/height
- 親のサイズが事前に決まっていないと動作しない(循環参照を避けるため)
-
Inline 要素の特性
display: inlineではwidth、height、margin-top、margin-bottomは無視される
-
Whitespace の扱い
- デフォルトでは HTML の改行は空白として扱われ、連続する空白は 1 つに縮約される
<pre>は空白の縮約を防ぐが、先頭と末尾の挙動に癖がある- 多くの場合、コンテンツ先頭/末尾の空白は無視されるが、
<a>は例外 inline-block間の空白や改行は実際の間隔として表示される(flex/grid では発生しない)
-
text-align
- テキストおよび inline 要素の整列には適用されるが、block 要素の整列には適用されない
-
box-sizing
- デフォルト値は
content-box→ padding/border は含まれない width: 100%+paddingを設定すると親領域をはみ出す可能性がある- 解決策:
box-sizing: border-box
- デフォルト値は
-
Cumulative Layout Shift
<img>にwidthとheight属性を指定しないと、画像読み込みの遅延によりレイアウトの揺れが発生する- 推奨: 属性を指定して CLS を防ぐ
-
Chrome におけるファイルダウンロードのネットワークリクエスト
- DevTools のネットワークパネルには表示されない(別タブで処理される)
- 分析が必要なら
chrome://net-export/を使う
-
HTML 内の JavaScript パース問題
<script>console.log('</script>')</script>のような場合、最初の</script>が終了タグとして認識される- 参考: Safe JSON in script tags
Unicodeとテキストエンコーディング
-
コードポイントと書記素クラスター
- 書記素クラスターは GUI における「文字単位」
- 可視 ASCII 文字は、コードポイント 1 個 = 書記素クラスター 1 個
- 絵文字は、複数のコードポイントで構成された 1 つの書記素クラスターである場合がある
- UTF-8 ではコードポイントは 1〜4 バイトで、バイト数とコードポイント数は一致しない
- UTF-16 ではコードポイントは 2 バイトまたは 4 バイト(サロゲートペア)
- 標準ではクラスター内のコードポイント数に制限はないが、実装では性能上の制限がある
-
言語ごとの文字列挙動の違い
- Rust: 内部文字列は UTF-8 を使用し、
len()はバイト数、直接インデックス参照は不可、chars().count()はコードポイント数、UTF-8 の妥当性を厳格に検証する - Golang: 文字列は事実上バイト配列で、長さとインデックス参照はバイト単位、主に UTF-8 を使用する
- Java, C#, JS: UTF-16 ベースで、長さは 2 バイト単位で測定され、インデックス参照も 2 バイト単位、サロゲートペアが存在する
- Python:
len()はコードポイント数を返し、インデックス参照では 1 つのコードポイントを含む文字列を返す - C++:
std::stringにはエンコーディング制約がなく、バイトベクターのように動作し、長さ/インデックス参照はバイト単位 - 挙げられた言語の中で、書記素クラスター単位で長さやインデックス参照を行う言語はない
- Rust: 内部文字列は UTF-8 を使用し、
-
BOM (Byte Order Mark)
- 一部のテキストファイルは BOM を持つ。例: EF BB BF → UTF-8 エンコーディングを示す
- 主に Windows で使われ、Windows 以外のソフトウェアでは BOM を処理できないことがある
-
その他の注意点
- バイナリデータを文字列に変換する際、壊れた部分は � (U+FFFD) に置き換えられる
- Confusable characters が存在する(互いによく似て見える文字)
- 正規化(Normalization): 例) é は U+00E9(単一コードポイント)または U+0065+U+0301(2つのコードポイント)で表現可能
- Zero-width characters および Invisible characters が存在する
- 改行コードの違い: Windows は CRLF
\r\n、Linux/MacOS は LF\n - 漢字統合(Han unification): 言語ごとに字形が少し異なる文字が同じコードポイントを使う
- フォントが言語別の異体字を含めて適切にレンダリングする
- 国際化では正しいフォントバリアントの選択が必要
浮動小数点 (Floating point)
-
NaN の性質
- NaN は自分自身を含むどの値とも等しくない(
NaN == NaNは常に false) NaN != NaNは常に true- NaN を含む演算結果はたいてい NaN として伝播する
- NaN は自分自身を含むどの値とも等しくない(
-
特殊な値
- +Inf と -Inf が存在し、NaN とは異なる
- -0.0 は +0.0 と区別される値
- 比較演算では同一だが、一部の計算では異なる動作をする
- 例:
1.0 / +0.0 == +Inf,1.0 / -0.0 == -Inf
-
JSON との互換性
- JSON 標準では NaN と Inf は許可されない
- JS
JSON.stringifyは NaN、Inf をnullに変換する - Python
json.dumps(...)は NaN、Infinity をそのまま出力する(標準違反)allow_nan=Falseオプション指定時は NaN/Inf があるとValueErrorが発生する
- Golang
json.Marshalは NaN/Inf が存在するとエラーを返す
- JS
- JSON 標準では NaN と Inf は許可されない
-
精度の問題
- 浮動小数点の直接比較は失敗する可能性がある →
abs(a - b) < ε形式を推奨 - JS はすべての数値を浮動小数点として扱う
- 安全な整数範囲は
-(2^53 - 1)~2^53 - 1 - この範囲を外れると整数表現が不正確になる
- 大きな整数には
BigIntの使用を推奨 - JSON に安全範囲を超えた整数が含まれると、
JSON.parseの結果値は不正確になる可能性がある - ミリ秒単位のタイムスタンプは 287,396 年先まで安全だが、ナノ秒単位では問題が発生する
- 安全な整数範囲は
- 浮動小数点の直接比較は失敗する可能性がある →
-
演算法則が成り立たない
- 演算順序による精度損失のため、結合法則や分配法則は厳密には成り立たない
- 並列演算(行列積、総和など)は非決定的な結果を生むことがある
-
性能
- 除算は乗算よりはるかに遅い
- 同じ数で何度も割る場合は、逆数を先に求めて掛ける形に最適化できる
-
ハードウェアによる違い
- FMA(Fused Multiply-Add) 対応の有無: 一部のハードウェアでは中間計算をより高精度で行う
- Subnormal range の処理: 最新ハードウェアは対応しているが、一部の旧世代では 0 として扱う
- 丸めモードの違い
- RNTE(最も近い偶数への丸め)、RTZ(0 方向への切り捨て)などがある
- x86/ARM ではスレッドローカルな mutable 状態として設定可能
- GPU では命令単位で丸めモードが異なる
- 三角関数、対数など数学関数の動作差
- x86 にはレガシーな 80 ビット FPU と per-core rounding mode が存在する → 使用非推奨
- このほかにもさまざまな要因で、ハードウェアごとに浮動小数点の結果が変わりうる
-
精度向上の方法
- 計算グラフを浅く構成する(乗算が連続する構造を減らす)
- 中間値が極端に大きい、または極端に小さいケースを避ける
- FMA のようなハードウェア演算を活用する
時間 (Time)
-
うるう秒(Leap second)
- Unix タイムスタンプはうるう秒を無視する
- うるう秒が発生すると前後の区間で時間を伸縮させることがある(Leap smear)
-
タイムゾーン(Time zone)
- UTC と Unix タイムスタンプは世界共通
- 人間が読む時刻は地域ごとのタイムゾーンに依存する
- DB にはタイムスタンプを保存し、UI で変換する方式を推奨
-
サマータイム(DST)
- 一部地域では夏季に時計を 1 時間調整する
-
NTP 同期
- 同期の過程で時刻が「後戻りする」状況が発生しうる
-
サーバーのタイムゾーン設定
- サーバーは UTC に設定することを推奨
- 分散システムでノードごとにタイムゾーンが異なると問題が発生する
- システムのタイムゾーン変更後は DB の再設定または再起動が必要
-
ハードウェアクロック vs システムクロック
- ハードウェアクロックにはタイムゾーンの概念がない
- Linux: ハードウェアクロックを UTC として扱う
- Windows: ハードウェアクロックをローカル時刻として扱う
Java
==はオブジェクト参照の比較であり、オブジェクト内容の比較には.equalsを使う必要があるequalsとhashcodeをオーバーライドしないと、map/set ではオブジェクト同一性が参照ベースで判断される- map の key オブジェクトや set の要素オブジェクトの内容を変更すると、コンテナの動作が壊れる
List<T>を返すメソッドは、場合によって mutable なArrayListまたは immutable なCollections.emptyList()を返し、後者を変更するとUnsupportedOperationExceptionが発生するOptional<T>を返すメソッドがnullを返すことがある(推奨されない)finallyブロックで return すると、tryまたはcatchで発生した例外が無視され、finallyの戻り値が適用される- interrupt を無視するライブラリが存在し、IO を含むクラス初期化の過程が interrupt によって壊れることがある
- スレッドプールで
.submit()に渡した task の例外は、デフォルトではログに出力されず future でしか確認できないため、future を無視すると例外を確認できないscheduleAtFixedRateの処理は例外が発生すると静かに停止する
- 数値リテラルが 0 で始まると 8 進数として扱われる(
0123→ 83) - デバッガはローカル変数の
.toString()を呼び出すため、一部クラスではtoString()に副作用があり、デバッグ時にコードの動作が変わることがある(IDE で無効化可能)
Golang
append()は capacity に余裕があるとメモリを再利用し、subslice への append で親のメモリまで上書きすることがあるdeferは関数の return 時に実行され、ブロックスコープ終了時ではないdeferは mutable な変数をキャプチャするnil関連- nil slice と empty slice は異なる
- string は nil にできず、空文字列のみ存在する
- nil map は読み取りは可能だが書き込みはできない
- interface nil の特殊な動作: data pointer が null でも type info が null でなければ
nilと等しくない
- Dead wait: Go には実際の並行性バグ事例が存在する
- Timeout の種類 は多様で、net/http で詳しく扱われている
C/C++
std::vectorの要素ポインタを保存した後に vector が grow すると再割り当てが発生し、ポインタが無効化される- リテラル文字列から生成された
std::stringは一時オブジェクトである可能性があり、c_str()呼び出し時に危険 - 反復中にコンテナを変更すると iterator の無効化が発生
std::removeは実際の削除ではなく要素の再配置であり、削除にはeraseが必要- 数値リテラルが 0 で始まると 8 進数として扱われる (
0123→ 83) - Undefined behavior (UB): 最適化の過程で UB は自由に変化し得るため、依存すると危険
- 初期化されていないメモリへのアクセスは UB
char*を struct ポインタに変換する場合、オブジェクト寿命の開始前アクセスにより UB となるため、memcpyによる初期化を推奨- 不正なメモリアクセス(null ポインタなど)は UB
- 整数の overflow/underflow は UB(unsigned は 0 未満への underflow が可能)
- Aliasing: 異なる型のポインタが同じメモリを参照すると strict aliasing rule により UB が発生
- 例外: 1) 継承関係にある型 2)
char*,unsigned char*,std::byte*への変換(逆変換には適用されない) - 強制変換には
memcpyまたはstd::bit_castを推奨
- 例外: 1) 継承関係にある型 2)
- Unaligned memory へのアクセスは UB
- メモリ Alignment
- 64 ビット整数はアドレスが 8 で割り切れる必要がある
- ARM では unaligned access でクラッシュする可能性がある
- バイトバッファを struct として直接解釈すると alignment 問題が発生
- alignment は struct padding を生み、メモリの無駄につながることがある
- 一部の SIMD 命令(AVX など)は整列されたデータしか処理できず、通常は 32 バイト alignment が必要
Python
- 関数のデフォルト引数は呼び出しのたびに新しく生成されず、最初の値がそのまま保持される
SQL Databases
-
Null の扱い
x = nullは動作せず、x is nullを使う必要がある- Null は自分自身と等しくない(NaN に似ている)
- Unique index は Null の重複を許容する(ただし Microsoft SQL Server は例外)
select distinctにおける Null の扱いは DB ごとに異なるcount(x)とcount(distinct x)は Null 値がある行を無視する
-
一般的な動作
- 日付の暗黙変換は timezone に依存する場合がある
- 複雑な join + distinct は入れ子クエリより遅い場合がある
- MySQL(InnoDB) で string フィールドが
utf8mb4でない場合、4-byte UTF-8 文字を挿入するとエラーが発生 - MySQL(InnoDB) はデフォルトで 大文字小文字を区別しない
- MySQL(InnoDB) は暗黙変換を許容する:
select '123abc' + 1;→ 124 - MySQL(InnoDB) の gap lock は deadlock を引き起こす可能性がある
- MySQL(InnoDB) では group by と select カラムが一致しない場合、非決定的な結果を返す
- SQLite では
strictでなければフィールド型にはあまり意味がない - Foreign key は暗黙の lock を発生させ、deadlock を引き起こす可能性がある
- Locking は DB によっては repeatable read isolation を破る可能性がある
- 分散 SQL DB は locking をサポートしていない、または特異な動作をする可能性がある(DB ごとに異なる)
-
性能/運用
- N+1 query 問題は各クエリが高速であるため、slow query log には現れない
- 長時間実行されるトランザクションは lock 問題などを引き起こすため、トランザクションは素早く終えることが推奨される
- テーブル全体 lock の事例
- MySQL 8.0+ では unique index/foreign key の追加時、多くの場合は並行処理が可能
- 古い MySQL バージョンではテーブル全体 lock が発生する可能性がある
mysqldumpに--single-transactionオプションがないと、テーブル全体に read lock がかかる- PostgreSQL で
create unique indexやalter table ... add foreign keyはテーブル全体の read lock を引き起こす- 回避策:
create unique index concurrentlyを使用 - foreign key は
... not validの後にvalidate constraintを使う方法を利用
- 回避策:
-
Range クエリ
- 重ならない範囲:
- 単純条件
p >= start and p <= endは非効率(複合インデックスがあっても) - 効率的な方法:
(start カラムのインデックスだけが必要)select * from (select ... from ranges where start <= p order by start desc limit 1) where end >= p
- 単純条件
- 重なり得る範囲:
- 一般的な B-tree インデックスでは非効率
- MySQL では spatial index、PostgreSQL では GiST の使用を推奨
- 重ならない範囲:
Concurrency and Parallelism
-
volatile
volatileは lock の代わりにはならず、atomicity も提供しない- lock で保護されたデータには
volatileは不要(lock が memory order を保証するため) - C/C++:
volatileは一部の最適化を防ぐだけで、memory barrier は追加されない - Java:
volatileアクセスは sequentially-consistent ordering を提供する(必要に応じて JVM が memory barrier を挿入) - C#:
volatileアクセスは release-acquire ordering を提供する(必要に応じて CLR が memory barrier を挿入) - メモリの読み書きの再順序化に関する誤った最適化を防げる
-
TOCTOU (Time-of-check to time-of-use) 問題
-
SQL DB におけるアプリケーション層での制約条件処理
- 単純な unique index では表現できない制約(例: 2 つのテーブル間でのユニーク、条件付きユニーク、期間内ユニーク)をアプリケーション側で強制する場合:
- MySQL(InnoDB): repeatable read レベルで
select ... for updateの後に insert し、かつユニーク列にインデックスがあれば、gap lock のおかげで有効(ただし gap lock は高負荷時に deadlock を引き起こす可能性があるため、deadlock detection と retry が必要) - PostgreSQL: repeatable read レベルで同じロジックを使っても、並行実行時には不十分(write skew 問題)
- 解決策:
- serializable isolation level を使用
- アプリケーションではなく DB 制約を使用
- 条件付きユニーク → partial unique index
- 2 テーブル間ユニーク → 別テーブルに重複データを挿入して unique index を設定
- 期間の排他性 → range type + exclude constraint
- 解決策:
- MySQL(InnoDB): repeatable read レベルで
- 単純な unique index では表現できない制約(例: 2 つのテーブル間でのユニーク、条件付きユニーク、期間内ユニーク)をアプリケーション側で強制する場合:
-
Atomic reference counting
Arc,shared_ptrのように多くのスレッドが同じカウンタを頻繁に変更すると性能が低下する
-
Read-write lock
- 実装によっては read lock から write lock へのアップグレードをサポートしない
- read lock を保持したまま write lock を試みると deadlock が発生する可能性がある
Common in many languages
- Null/None/nil チェックの漏れはよくあるエラーの原因
- ループ中にコンテナを変更すると、単一スレッドのデータ競合が発生する可能性がある
- 可変データ共有のミス: 例) Pythonで
[[0] * 10] * 10は正しい2D配列の生成ではない (low + high) / 2はオーバーフローする可能性がある → 安全な方法はlow + (high - low) / 2- 短絡評価(short circuit):
a() || b()は a が true なら b は実行されず、a() && b()は a が false なら b は実行されない - プロファイラのデフォルトは CPU time のみを含む → DB待機などは flamegraph に現れず、誤解を招く
- 正規表現の dialect は言語ごとに異なる → JSで動く正規表現がJavaでは動かないことがある
Linux and bash
- ディレクトリ移動後、
pwdは元のパスを示し、実際のパスはpwd -P cmd > file 2>&1→ stdout+stderr の両方がファイルへ、cmd 2>&1 > file→ stdout のみファイルへ、stderr はそのまま- ファイル名は 大文字小文字を区別 する(Windowsとは異なる)
- 実行ファイルには capability システムが存在する(
getcapで確認) - 未設定変数の危険性:
DIRが unset の場合、rm -rf $DIR/→rm -rf /実行の危険 →set -uで防止可能 - 環境の反映: スクリプトを現在の shell に反映するには
source script.shを使う → 永続的に反映するには~/.bashrcに追加 - Bash は コマンドをキャッシュする:
$PATH内のファイル移動時にENOENTが発生 →hash -rでキャッシュ更新 - 変数を引用せずに使うと改行が空白として扱われる
set -e: スクリプトでエラーが起きると即時終了するが、条件文の内部(||,&&,if)では動作しない- K8s livenessProbe とデバッガの衝突: ブレークポイントデバッガはアプリ全体を停止させ、health check 応答に失敗 → Pod が終了する可能性がある
React
- レンダリングコードで state を直接変更
- Hook を if/loop の中で使用 → ルール違反
useEffectの dependency array に必要な値が不足useEffectで クリーンアップ(clean up)コードの漏れ- Closure trap: 古い state をキャプチャしてバグが発生
- 間違った場所でデータを変更 → 不純なコンポーネント
useCallbackの使用漏れ → 不要な再レンダリングが発生- メモ化されたコンポーネントに 非メモ化の値を渡す と memo 最適化が無効になる
Git
-
Rebase は履歴の書き換え
- rebase 後に通常の push をすると衝突 → 必ず force push が必要
- remote branch の履歴を変更した場合、pull も
--rebaseを使用 --force-with-leaseは一部のケースで他の開発者の commit の上書きを防げるが、fetch だけして pull していないと保護されない
-
Merge revert の問題
- Merge revert は効果が不完全 → 同じブランチを再度 merge しても変化がない
- 解決策: revert の revert を実行する、またはクリーンな方法(backup → reset → cherry-pick → force push)
-
GitHub 関連の注意事項
- API キーのような secret を commit した後、force push で上書きしても GitHub には記録が残る
- private repo A を fork した B が private でも、A が public になると B の内容も公開される(削除後もアクセス可能)
-
git stash pop: conflict 発生時、stash は drop されない -
.DS_Storeは macOS が自動生成 →.gitignoreに**/.DS_Storeの追加を推奨
Networking
- 一部の ルーター・ファイアウォールはアイドル状態の TCP 接続を静かに切断する → HTTP クライアント・DB クライアントのコネクションプールが無効化される可能性 → 解決策: TCP keepalive を設定
tracerouteの結果は 信頼性が低い → 場合によってはtcptracerouteのほうが有用- TCP slow start はレイテンシ増加の原因 →
tcp_slow_start_after_idleを無効化して解決可能 - TCP sticky packet 問題: Nagle アルゴリズムはパケット送信を遅延させる →
TCP_NODELAYを有効にして解決可能 - Nginx の背後にバックエンドを配置する場合、コネクション再利用の設定が必要 → 未設定だと高負荷環境で内部ポート不足により接続失敗
- Nginx はデフォルトで パケットをバッファリングする → SSE(EventSource) の遅延が発生
- HTTP 標準は GET・DELETE リクエストの body を禁止していない → body を使う例もあるが、多くのライブラリ・サーバーは対応していない
- 1つの IP で複数の Web サイトをホスティング可能 → 振り分けは HTTP
Hostヘッダと TLS の SNI が担当 → 単純な IP アクセスでは表示できないサイトが存在 - CORS: 異なる origin へのリクエスト時、ブラウザはレスポンスへのアクセスを遮断 → サーバー側で
Access-Control-Allow-Originヘッダの設定が必要- Cookie 送信を含む場合は追加設定が必要
- フロントエンドとバックエンドが 同一ドメイン・同一ポート なら CORS の問題はない
Other
-
YAML の注意点
- YAML は 空白に敏感 →
key:valueはエラー、key: valueが正しい - 国コード
NOは引用符なしで書くとfalseと解釈される問題が発生 - Git commit hash を引用符なしで書くと数値に変換される可能性がある
- YAML は 空白に敏感 →
-
Excel の CSV 問題
- Excel は CSV を開くと 自動変換を行う
- 日付変換:
1/2,1-2→2-Jan - 大きな数値の不正確な変換:
12345678901234567890→12345678901234500000
- 日付変換:
- 原因は Excel が内部的に数値を floating point として扱うため
- この問題により、遺伝子名 SEPT1 が誤って変更された事例がある
- Excel は CSV を開くと 自動変換を行う
まだコメントはありません。