世界を動かす技術を、日本語で。

開発者への罠

概要

  • 開発者が陥りやすい HTML/CSS、Unicode、浮動小数点、時間 に関する落とし穴の要約
  • 直感に反する挙動誤解 しやすい仕様を中心に整理
  • バグの原因 となりやすいポイントを明確化
  • 各分野ごと に代表的なトラップを箇条書きで説明
  • 対策や回避策も簡潔に記載

HTML・CSSの落とし穴

  • flexboxやgrid内 では、デフォルトで min-width: auto が設定され、内容に応じて最小幅が決まる
    • min-width: 0 を明示的に指定することで、flex-shrinkやoverflow等の他プロパティが正常に動作
  • width: autoheight: auto の挙動は異なる
    • 横方向は親要素の空間を埋めるが、縦方向は内容に合わせて伸びる
    • inline要素やinline-block要素、float要素 ではwidth: autoは拡張しない
  • margin: 0 auto は水平方向中央寄せ
    • margin: auto 0 は通常上下中央寄せにならない
    • flex-direction: column のflexboxでのみmargin: auto 0が垂直中央寄せとなる
  • マージンの折り畳み(margin collapse) は垂直方向のみ発生
    • BFC(Block Formatting Context)やborder/padding指定で回避可能
    • display: flow-rootoverflow: hidden 等でBFCを作成
  • 親がfloat子要素のみ含む場合、親の高さが0になる
    • BFCで解決可能
  • スタッキングコンテキスト の発生条件
    • transform, filter, opacity等の特殊効果
    • position: fixed/sticky、z-index指定+position: absolute/relative
    • isolation: isolate
  • z-indexはスタッキングコンテキストをまたいで効かない
    • position: absolute/fixedは最も近いpositioned ancestor基準
  • 100vh問題
    • モバイルブラウザのアドレスバー表示/非表示で高さが変動
    • 100dvh が推奨値
  • position: absolute は親要素ではなく最も近いpositioned ancestor基準
  • display: flexやgridの親下でfloatは無効
  • 親のwidth/height未定義時、%指定のwidth/heightは効かない
  • display: inline はwidth/heightやmargin-top/bottom無効
  • HTMLの空白・改行はデフォルトで1つの空白にまとめられる
    • <pre> で回避可能だが、先頭・末尾の空白は特殊挙動
    • inline-block要素間の空白 はスペースとして描画(flexboxやgridでは発生しない)
  • text-alignはインライン要素のみ整列
  • width/heightはデフォルトでpadding/borderを含まない
    • box-sizing: border-box で全体幅に含める
  • 画像の遅延読み込みによるCumulative Layout Shift
    • <img>にwidth/height属性指定 推奨
  • ファイルダウンロードリクエストはChrome DevToolsで見えない
    • chrome://net-export/ で確認

Unicode・テキスト処理の落とし穴

  • コードポイント(rune)とグラフェームクラスタ の違い
    • GUI上の「1文字」はグラフェームクラスタ
    • 絵文字は複数コードポイントから成ることも
  • UTF-8は1~4バイト、UTF-16は2バイト単位
    • 文字数・インデックスの扱いは言語ごとに異なる
  • Rust: UTF-8、s.len()はバイト数、s.chars().count()はコードポイント数
  • Go: 文字列はバイト列、インデックスもバイト単位
  • Java/C#/JS: UTF-16、長さ・インデックスは2バイト単位
  • Python: len(s)はコードポイント数、インデックスもコードポイント単位
  • C++: std::stringはエンコーディング非依存、バイト単位
  • グラフェームクラスタ単位の長さ・インデックス取得は標準で不可
  • BOM(Byte Order Mark) の扱い
    • 一部ソフトで非対応
  • バイナリ→文字列変換時の不正箇所はU+FFFD(�)に置換
  • 正規化・類似文字 問題
    • 例:éはU+00E9またはU+0065 U+0301
  • ゼロ幅・不可視文字 の混入
  • 改行コードの違い
    • Windows: CRLF (\r\n)、Linux/Mac: LF (\n)
  • 漢字統合(Han unification) によるフォント依存表示
    • 多言語対応ではフォント選択が重要

浮動小数点の落とし穴

  • NaNは何とも等しくない(NaN == NaNはfalse)
    • 計算にNaNが混入すると全体がNaN化
  • +Infと-InfはNaNと別物
  • -0.0と+0.0は区別される
    • 1.0 / +0.0は+Inf、1.0 / -0.0は-Inf
  • JSON標準はNaN/Inf非対応
    • JS: nullに変換
    • Python: allow_nan=Falseでエラー
    • Go: エラー
  • 浮動小数点の比較は誤差を考慮
    • abs(a - b) < 0.00001等で比較
  • JavaScriptの安全な整数範囲
    • 2^53-1までが安全、それ以上はBigInt推奨
    • 大きな整数をJSONで扱う場合は文字列化
  • 結合法則・分配法則が厳密には成立しない
    • 並列計算で非決定的結果になることも
  • 除算は乗算より遅い
    • 逆数を先に計算して乗算で高速化
  • ハードウェアによる差異
    • FMA(fused multiply-add)やサブノーマル数
    • 丸めモード(RNTE, RTZ等)、CPU/GPUでの挙動違い
    • X86の80bit FPUは非推奨
  • 精度向上の工夫
    • 計算グラフの浅さ、FMA活用、中間値の絶対値管理

時間処理の落とし穴

  • Unixタイムスタンプはうるう秒を考慮しない
    • うるう秒付近で「引き伸ばし」や「圧縮」が発生
  • タイムゾーン・DST(夏時間)の影響
    • タイムスタンプ保存+UIで人間可読化が推奨
    • サーバーはUTC設定が望ましい
    • 分散システムでタイムゾーン不一致は厄介
  • システム時刻とハードウェア時刻の違い
    • タイムゾーン変更時はDB再起動等が必要な場合あり
  • NTP同期等による時刻の「巻き戻り」
    • 時間の一意性・単調性に注意

上記以外にも、各分野で 直感に反する仕様や落とし穴 が多く存在。 実装前に仕様書や公式ドキュメントの確認 が重要。

Hackerたちの意見

これは役立つ情報やヒントがまとめられてていいね。誰かがこれから学べるといいけど。自分は、既に知ってることしか理解できなかった気がするし、知ってることにすごく近かっただけかも。それって、どんなトピックの教科書でも同じことが起こるよね。内容は整理されてて、既に知識がある人にはわかりやすいけど、何も知らない人には教えるのがすごく難しい。

どんなトピックの教科書でも同じことが起こるよね。内容は整理されてて、既に知識がある人にはわかりやすいけど それがマニュアルが存在する理由だと思う。記録を残しておくことで、記憶を頼らなくて済むから。これがほとんどのUnixマニュアルの役割だよね。ソフトウェアが何をできるかは知ってるけど、どうやって特定のことをやるかを思い出す必要がある。 > 何も知らない人には教えるのがすごく難しい。だから必要なのは、初心者向けのチュートリアル(学びたい人)か、ガイド(やりたいことをするための初心者/中級者向け)だね。この場合、マニュアルはより良い質問をするためだけに役立つ(今、自分が知らないことがわかるから)。

最近の罠:正規表現の意味は言語によって微妙に違うんだよね。例えば、Pythonでは a{,3} は0から3個の「a」文字にマッチするけど、JavaScriptではリテラル文字列の「a{,3}」にマッチする。

正規表現は実際の仕様というよりは技術に近いね。基礎的なメカニズムを説明している計算理論の入門書を読む時間を見つけるのが一番いいと思う。

自分はいつもregex101を使って正規表現を作成してる。いろんなエンジンに切り替えられるから便利だよ。

あと一歩でお気に入りのトラップって感じの[a-z]に名誉の言及。

CSSとC++は「サブセットを選んでそれを強制するか、さもなくば苦しむ」っていう性質があるよね。やることリストには、CSS属性がまだ存在しないプルリクエストをマージするには手動でオーバーライドが必要なGitHubアクションを作るって書いてある。

CSSがどう機能するのか、ちょっと不明だな。私の知識では、ほとんどのCSSプロパティは互換性がないと思う。もし強制されるサブセットが「すでに存在するCSSプロパティ」なら、開発者はそのプロパティが存在しない場合、どうすればいいの?デザインを変えるの?

ページの最初の「罠」には「min-width: autoはコンテンツによって最小幅が決まる」と書いてあるけど、フレックスやグリッド以外ではこれは間違いだよ。MDNによると、「ブロックボックス、インラインボックス、インラインブロック、すべてのテーブルレイアウトボックスでは、autoは0に解決される。」だって。 https://developer.mozilla.org/en-US/docs/Web/CSS/min-width

最初の罠は実際には「CSSプロパティは孤立して読むことはできない。名前が示すように、デフォルトや値がどのようにカスケードするかは、ドキュメントが使用するすべてのルールに影響を与える」ってことだと思う。

CSSのテキストプロパティのカスケードは、まあまあ理解できるけど、CSSのレイアウトはどの視点からも全然わからん。ページデザイナー、実装者、ユーザー、何でもいいけど、誰を想定してるのか全くわからん。

ありがとう、修正するよ。

Java、C#、JSはメモリ内の文字列にUTF-16風のエンコーディングを使ってる それはJavaに関しては間違いだし、C#やJSもそうかもしれん。文字列が十分に不透明な型[1]であれば、メモリ内の表現は実装の詳細に過ぎない。Javaはリリース9以降、そういう言語になってる(https://openjdk.org/jeps/254)。[1] 「十分に」というのは、いくつかの言語では完全に不透明な型があって、特定の操作の効率を指定することで、実装の詳細を事実上禁止しているから。外部関数インターフェースを持つことは、実装の詳細を変更できないことが多い。なぜなら、そうしないと後方互換性が壊れちゃうから。 > JSはすべての数値に浮動小数点を使ってる。最大の正確な整数は2⁵³−1 それは間違い。もっと大きな整数も正確に表現できる。例えば、2¹⁰⁰とかね。正しいのは、2⁵³−1がn-1、n、n+1がIEEEダブルで正確に表現できる最大の整数nだってこと。それによって、n == n-1とn == n+1がどちらもfalseになるのが「普通」の算術で期待されることだね。

C#やJSでもそうかもしれん C#の表現はかなり固定されてる。直接文字列バッファにアクセスすることができて、ReadOnlySpanや生のcharポインタを使うことが多いから、charはUTF-16のコードポイントの型だ。JSはもしかしたらそれでやっていけるかも。

C#の文字列について何か言おうとしたら、Windows開発と文字列のクラスター・ファックを思い出した。どのAPIを呼ぶかによって、文字列が十数通りの方法で表現されるんだよね。https://stackoverflow.com/questions/689211/interop-sending-s...

うん、彼らは「正確な」整数の最大値ではなく、「安全な」整数の最大値を意味してたんだと思う。

Java、C#、JSはメモリ内文字列にUTF-16のようなエンコーディングを使ってる > > それはJavaには当てはまらないよ。技術的にはそうかもしれないけど、UTF-8(または別のエンディアンのUTF-16)を使う言語で文字列をBase64エンコードして、それをJavaでデコードすると、JavaのUTF-16表現が問題になるんだ。

ありがとう、修正するね。

これ、トラップというよりは、著者が学んだことのリストみたいだな。多くは特定の狭い文脈でしか適用できないけど、その文脈は必ずしも言及されてない。一部は単に間違ってるように見える。要するに、これを文字通り受け取るべきじゃなくて、ほぼ意識の流れみたいなものとして捉えた方がいいと思う。

Optionalを返すメソッドはnullを返すことがある こういうプロジェクトはマジでイライラする。もし感情的エネルギーがあれば、新しい@java.lang.NonNullReferenceのJEPを開くんだけど、それに注釈が付けられた型にはnullを代入するのがコンパイラーエラーになる。public interface Alpha {} @java.lang.NonNullReference public interface Beta {} Alpha a = null; // ok Beta b = null; // コンパイラーエラー javacはこれを許容する Beta b; if (Random.randBoolean()) { b = getBeta(); } else { b = newBeta(); } でも、デッドコードの排除がニッチなのか形式的なものなのか、言語仕様をじっくり見ないといけない。Beta b; if (true) { b = getBeta(); } else { b = null; // これが省略されて、技術的には合法になると思う。

Optionalを持つこと自体が、nullのある言語では賢明かどうか疑問だな。Pythonで関数がOptional型のオブジェクトを返すと、T | Noneの方がいいって眉をひそめるだろうし。可愛いモナド的なことをやらない限り、どちらにしてもチェックが必要だよね。

Kotlinだと、これもうコンパイルエラーになるから、別のアノテーションはいらないよ。

一部のルーターやファイアウォールは、アプリケーションに知らせずにアイドル状態のTCP接続を静かに切断することがあるんだ。HTTPクライアントライブラリやデータベースクライアントみたいに、再利用のためにTCP接続のプールを持ってるコードがあって、それが静かに無効化されることがある。これを解決するには、システムのTCPキープアライブを設定することができるよ。HTTPの場合は、Connection: keep-aliveやKeep-Alive: timeout=30, max=1000ヘッダーを使うといい。TCP接続が確立されると、接続の両端の間にあるルーターには状態がないんだ。問題はファイアウォールやNATエントリのタイムアウトなんだよね。実際、RSTは送信されないし。K8sでconntrackモジュールの設定が低すぎてこの問題が発生したことがあった。HTTPキープアライブを入れてみることはできるけど、それはあまり役に立たないよ。HTTPキープアライブはHTTPレベルでの接続再利用のためのもので、接続を閉じるわけじゃないからね。https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/... HTTPキープアライブはパッケージを生成しないし、単に閉じるのを遅らせるだけなんだ。TCPキープアライブはパッケージを生成してタイマーをリセットするんだよ。

ありがとう、修正するよ。

s/packages/packets/

いいリスト記事だと思うけど、いくつかフィードバックがあるよ。> Unicodeの統一。異なる言語の異なる文字が同じコードポイントを使ってるんだ。異なる言語のフォントバリアントが同じコードポイントを異なってレンダリングすることもある。語 これは罠じゃないよ。与えられた例の文字は、中国語と日本語で同じ意味を持っていて、日本語版は中国から輸入されたものなんだ。両方の言語の人々は、両方のフォントバリアントを同じ概念の文字として認識しているんだ。著者は、英語の'A'はフランス語の'A'とは別のコードポイントを持つべきだと言ってるみたいだけど、実際にはそんなことはないよね。あるいは、上の尾がある小文字の'a'は、上の尾がない小文字の'a'とは別の文字であるべきだとも言ってる。とにかく、これについてはhttps://en.wikipedia.org/wiki/Han_unificationで詳しく議論されてるよ。> 負のゼロ-0.0があり、通常のゼロとは異なるんだ。負のゼロは浮動小数点比較を使うとゼロと等しい。通常のゼロは「正のゼロ」として扱われるんだ。そして、負のゼロと通常のゼロを区別する方法は2つあるよ:整数ビットパターンで、または1.0/-0.0 == -Infと1.0/0.0 == +Infという事実で。> サーバーのタイムゾーンをUTCに設定することを推奨するよ。大賛成。サーバー、ログ、写真、アーカイブして正しくタイムスタンプを付ける価値のあるものにはUTCを使ってる。ローカルタイムは口語的な使用だけだね。> 整数の場合、(low + high) / 2はオーバーフローする可能性がある。より安全な方法はlow + (high - low) / 2だよ。そうだけど、lowとhighが負の数になる可能性があるなら、オーバーフローを別の範囲にシフトさせただけだよね。これは、整数範囲に対する一般的な二分探索にとって重要なんだ。配列に対する符号なしの二分探索とは違うから。> C/C++ 俺はC/C++での落とし穴のリストを一つ投げ込むつもりだ。整数型と算術を正しく使うだけでも、開発者にとっては大きな罠なんだ。それってプログラミングの最も基本的なことだよ。https://www.nayuki.io/page/summary-of-c-cpp-integer-rules > リベースは履歴を書き換えることができる。「できる」は言い回しだね。リベースは履歴を書き換える以外のことは何もしないんだ。

著者は、英語の'A'はフランス語の'A'とは別のコードポイントを持つべきだと言ってるみたいだけど、実際にはそうじゃないよね。あるいは、上の尾がある小文字の'a'は、上の尾がない小文字の'a'とは別の文字であるべきだとも言ってる。でも、АとAはあるし、見た目は同じだよね。統一された漢字はしばしばかなり異なることがあって、中国語を学ぶ者として何度も混乱させられたよ。例えば、非常に一般的な文字「喝」(飲む)はかなり違って見えるんだ:https://en.wiktionary.org/wiki/%E5%96%9D - 画数も異なるしね。ここにコピー&ペーストすることもできないし、Wikipediaの記事からコピーすると形が変わっちゃうから、実演できないんだ。漢字の統一は本当にややこしいね。

Python: - デフォルト引数は、毎回呼び出すたびに再生成されない保存された値なんだ。datetime変数を扱う人へのPSAだね!