詳細を明らかにする言語 ほとんどの人がプログラミングを始めるとき、物事を簡単にする言語に惹かれます。Python、JavaScript、その他の高水準言語は、メモリ管理、システム コール、ハードウェアの相互作用といった煩雑な詳細を抽象化します。この抽象化は強力で、初心者でも実装の詳細に悩まされることなく、便利なプログラムをすばやく作成できます。 しかし、こうした詳細に立ち向かわせる言語には大きな価値があります。Rust、C、Zig などの言語は、特定の言語でより優れたプログラマーになるだけでなく、コンピューターが実際にどのように動作するかについての理解を深めます。この理解により、高レベルの言語であっても、使用するすべての言語でより効果的に作業できるようになります。 デモでは、ユーザーからの入力を読み取って変数に格納するという「単純な」概念を取り上げ、それが高水準言語から低水準言語にどのように実行されるかを説明します。まずは最も高水準な言語から始めましょう。 パイソン name = input("What is your name?\n") print(name) #Ah, the classic I/O example 学習者にとって、ここでの疑問や学習とは何でしょうか? 覚えておいてください、私たちはただコードを書こうとしているのではなく、実際に何が起こっているのかを知ろうとしているのです。 データを保持する「変数」があります。 変数とメモリ: データ型があり、文字列は単なる通常のテキストです。好奇心旺盛な学習者であれば、このヒントから他のデータ型についても学ぶことができます。 データ型とメモリ: 引数を指定して関数を呼び出し、その関数の結果を変数に格納できます。 関数呼び出し: Python プログラムは、インタープリターを呼び出してプログラムを実行することで実行できます (Python がインストールされていることを前提としています。Python のバージョン管理、依存関係、およびインストールについてはここでは説明しません)。これにより、インタープリター型言語とコンパイル型言語の違いが明らかになる可能性があります。 ランタイム環境 これらは悪くありません。コンピューターに関する最大の知識は、文字列内の小さな '\n' から得られると思います。これを少し調べると、ASCII、UTF-8、およびコンピューター内のテキストのバイト表現に関する知識が得られます。初心者には すぎるかもしれませんが、テキストが 0 と 1 に変わる仕組みを するのに役立ちます。インタープリターとコンパイラーに関するレッスンもここにありますが、かなりの調査が必要です。 多 理解 Javascript/Typescript (ノード) import readline from 'readline/promises'; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const name = await rl.question('What is your name?\n'); rl.close(); console.log(name); //Oh, js, so horribly wonderful in your ways これまでの洞察に加えて、好奇心旺盛な学習者がこのコードを調べるだけで何がわかるかを評価してみましょう。 : これらを簡単に調べると、Unix ベースの環境の stdin と stdout ファイル ストリーム、さらには Linux システムのファイル記述子や「すべてがファイル」にたどり着くかもしれません。 入力/出力ストリーム stdin と stdout への明示的な参照があります。 オブジェクトを見ると、好奇心旺盛な人がプロセスについて知り、最新のオペレーティング システムの実行プロセスを垣間見るきっかけになるかもしれません。完全に理解できないかもしれませんが、大体の プロセス: プロセス アイデアは得られます。 : と Promise は、コンピューターがすぐに完了しない操作をどのように処理するかを学習者に紹介し、場合によっては、なぜ単純な方法 (Python など) で実行されないのかという疑問も生じさせます。これらは、次のことを学ぶきっかけになります。 非同期 I/O await 同期および非同期実行、非ブロッキングI/O、そしておそらく同時実行 Promise、Microtask キュー、Node のタスクキュー、イベント駆動型プログラミングとその利点。 インターフェイスの作成とクローズにより、特に I/O ストリームなどの重要なリソースに関するリソース管理について疑問が生じ、理解が深まります。 インターフェイスの作成とリソース管理: ( 、 ): これらはより深い概念に明示的にマッピングされませんが、可変性を制御するための優れたプラクティスを教えてくれます。 宣言キーワード let const JS プログラムは、Node、Bun、Deno などのランタイムを介して実行されます。ランタイムの役割は、 (JS エンジン) に完全な言語にするための追加機能を提供することです。これらのランタイムが V8 エンジンに正確に 提供するのか疑問に思う人もいるかもしれませんが、これは非同期 I/O の実装につながります。 ランタイム環境: V8 何を Promise や Queue など、一見すると JS 関連の抽象化のように見えるものもありますが、最終的に Node.js の非同期 I/O を処理する C ライブラリ) にたどり着くと、オペレーティング システムの I/O について少し学ぶことになります。 Libuv ( Cシャープ Console.WriteLine("What is your name?"); string? name = Console.ReadLine(); Console.WriteLine(name); //Surprise!! No public static void Main(string[] args) 文字エンコードの通常の容疑者は、 と によって隠されていますが、それ以外に 2 つの重要なことが浮かび上がります。 ReadLine WriteLine : 型推論は生産性を高める素晴らしい機能ですが、特に初心者にとっては、型を明示的に記述することで学習プロセスが改善されるという見解を維持しています。ここでは、学習者は、特に変数の型を明示的に指定する理由を探求することで、メモリ レイアウトについての最初の実際の概念を習得できます。これには、特定の変数に特定のバイト数を予約することや、64 バイトを 32 バイトのメモリに収めようとしたときに発生するエラーが含まれます。 静的型付けと明示的な型 メモリの場所に有効な値が存在しない可能性があることを学習し、メモリのビューをさらに強化します。 Null 許容型: 本当に好奇心旺盛な人は、なぜ null 許容型を明示的に指定する必要があるのか、プログラムで null 値を非 null 値として扱うことから生じる特定の問題はあるのか、と疑問に思うでしょう。これは、メモリ保護ルールについて学ぶことにつながります。 .NET ランタイムは、学習者に明示的に してからコード ように強制することで、コンパイル プロセスをより明確にします。 共通言語ランタイム (CLR)、中間言語 (IL)、および JIT: ビルド を実行する ユーザーにコードを強制的にコンパイルさせることで、生成された IL を見ることができます。これにより、アセンブリ (疑似アセンブリ)、命令、レジスタを初めて確認することができます。学習者が内部をもう少し詳しく調べれば、CLR の Just-In-Time コンパイルについて学習できる可能性もあります。 これらの概念は他の言語にも存在しますが、違いは、ユーザーに公開することで、ユーザーがすぐに深く理解し、コードを実行すると 何が起こるのかを理解できるようになることです。 実際に 最後に、 ストリームやリソース管理に関連するものは何もありません。 ここでは I/O が JS よりも抽象化されています。 Go言語 申し訳ありませんが、Gophers の皆さん、 この記事が長くなりすぎてしまいます。 すべてを網羅することはできません。そうすると、 さび use std::io; fn main() { println!("What is your name?"); let mut name = String::new(); io::stdin() .read_line(&mut name) .expect("Failed to read line"); println!("{}", name); } //Almost a 1:1 from The Book 適度に好奇心のある学習者にとって、システムの概念について理解できることは次のとおりです。 : キーワードは、変数がデフォルトで不変であることを示します。繰り返しますが、データの可変性を制御することで、そのすべての利点が得られます。 明示的な可変性 mut : 、I/O が失敗する可能性があることを示し、エラー処理の考慮を強制します。これは高級言語ではほとんど当然のこととされており、学習者は物理デバイスとのやり取りによって、事前に知らされていなければ考えも及ばないような多数のエラーが発生する可能性があることを理解できます。たとえば、データベース開発者にディスクが完璧かどうかを尋ねてみてください。 明示的なエラー処理 .expect() : OS レベルの I/O リソースとのやり取りを明示的に示します。以前と同様に、これにより OS の I/O 概念をより深く理解できますが、違いは、JS の場合よりも物事がはるかにシンプルになっていることです。 ダイレクト ストリーム アクセス io::stdin() : メモリの最も重要な 2 つの概念であるヒープとスタックに、疑似明示的ではありますが初めて遭遇することを示しています。あまり明示的ではありませんが、好奇心旺盛な学習者がメモリを簡単に探索し、「なぜ異なるメモリ領域が必要なのか?」「ヒープとは何ですか?」などの質問をするのに十分なヒントを提供します。 メモリ割り当て String::new() : への最初の明示的な導入を示しています。これまですべての言語が内部で参照を使用してきましたが、これをプログラマーに前面に出すことで、メモリレイアウトのより深い概念を習得できるようになります。プログラマーは、参照を使用するだけで複数の領域で同じデータを使用できること、およびそのようなアプローチの利点と危険性を学びます。 参照と借用 &mut name ポインター ここでも、ビルド ステップを明示的に要求すると、学習者はコンパイル プロセスの調査を開始しますが、今回は、アセンブリ命令のポイントまで調査し、最新の CPU の実行プロセスについて少し学ぶ機会があります。 コンパイラ、実行可能ファイル、およびアセンブリ: 高レベルの抽象化に慣れている場合でも、Rust で小さな要素を 1 つ試してみると、他の言語では隠されたままになっているシステム動作の世界全体が明らかになることがあります。これらのほとんどは新しいものではありませんが、違いは、ここではそれらがプログラマーに公開され、プログラマーがそれらについて考え、学習することを余儀なくされることです。これにより余分なオーバーヘッドと困難が生じますが、より深い理解が得られ、結果としてシステムのリソースに対する権限が得られます。 ジグ const std = @import("std"); pub fn main() !void { var debugAllocator = std.heap.DebugAllocator(.{}).init; defer std.debug.assert(debugAllocator.deinit() == .ok); const allocator = debugAllocator.allocator(); const stdout = std.io.getStdOut().writer(); const stdin = std.io.getStdIn().reader(); var name = std.ArrayList(u8).init(allocator); defer name.deinit(); try stdout.print("What is your name?\n", .{}); try stdin.streamUntilDelimiter(name.writer(), '\n', null); try stdout.print("{s}\n", .{name.items}); } //lol, the code block doesn't have support for Zig ヒープに割り当てられた「拡張可能な」文字列を含めるか、単に非常に大きなスタックに割り当てられた「静的な」文字列を含めるかについて 議論しましたが、他のすべての例で「拡張可能な」文字列を使用したため、ここで採用しました。簡単に説明すると、拡張可能な文字列は追加入力によって拡張できますが、静的文字列は固定されており、新しい文字を追加する唯一の方法は、新しい文字で新しい文字列を作成することです。 注: 本当に ああ、どこから始めればいいのでしょうか? 学習者がこのコードを見ると、おそらく怖くなって逃げてしまうでしょうが、これを調査することでシステムの概念について何が学べるでしょうか。 Zig では、ヒープからメモリを取得する必要がある場合、その意図を宣言する必要があることが明示されています。これは Rust ほど抽象化されていません。ここでは、それが明らかにされています。これにより、初期のオーバーヘッドは増えますが、開発者はスタック、ヒープ、動的メモリ、OS システムコール、さらにはメモリを明示的に割り当てたり解放したりする必要がある理由について調査し始めるようになります。これにより、開発者のメモリ構造に対する理解がさらに深まります。 アロケータとメモリ: メモリをクリーンアップするための明示的な 呼び出しとメモリ リークのチェックは、不適切に管理されたメモリから生じる問題を直接学習するための良い出発点です。この時点で、開発者はこのトピックについて深く理解するのに十分な準備が整っています。 クリーンアップとリーク検出: defer 文字列は、単に 値の配列へのポインタです。これにより、高レベルの「文字列」の概念と低レベルの「バイト配列」の考え方の間の最後の抽象化が取り除かれます。 文字列、スライス、および参照: u8 繰り返しになりますが、これらを可視化することで、開発者はプログラム外部の I/O から読み取りまたは書き込みを行うときに何が 理解できます。 I/O ストリームへの直接アクセス: 起こるかを I/O デバイスの呼び出し (および一般的なシステムコール) は失敗する可能性があります。開発者はこれを調査し、認識しておく必要があります。 I/O エラーとエラー処理: C および C++ 私がここで言いたいことはお分かりだと思います。無駄な議論をする必要はないのです。 というわけで、これで完了です。複数の言語で簡単な作業です。私の言いたいことがわかっていただけたと思います。ただし、その前にいくつか明確にしておきたいことがあります。 では、Rust で書き直すだけですか? いいえ、答えはノーです。ただノーです。私は、コンピューターの仕組みについてもっと学びたい人、そしてハードウェアからすべてを引き出す必要がある人(必要に応じて、 アセンブリを使用することもできます)の観点からこれらの議論をしています。 FFMPEG の人たちのように しかし、開発速度のために効率をある程度犠牲にしても構わない場合はどうなるでしょうか。1、2 日でロジック付きの軽量 Web サーバーを作成する必要がある場合はどうでしょうか。C++ に恐れをなしてコードの作成をやめたいと考えている新人開発者の場合はどうなるでしょうか。 Go、Elixir、Haskell などで十分対応できる状況は数多くあります。その後は、実際に何が起こっているのかを少し時間をかけて学んでください。寝ている間にも asm を書けるような低レベルの達人になる必要はありませんが、コンピューターで何が起きているのかを知っておくと また、コンピューターをブラック ボックスとして見なすのをやめるのに役立ちます。また、楽しんでいただけることをお約束します。 、より優れた、よりパフォーマンスの高いコードを書くのに役立ちます 。 で話しかけてください。 Twitter