プログラミング言語を学ぶ人・教える人に知っておいて欲しい、学習の難易度の考え方。 C# がどういう意味合いで「簡単」なのかについて。
プログラミング言語が簡単・難しいという話をするとき、 必ず出て来るのが以下のような対立です。
前者は How(どう書く、どう実装する)に基づくの難しさ、 後者は What(何がしたいか)に基づく難しさです。
プログラミングって手段であっても目的ではないわけで。What(何をしたいか)ありきだと思います。
if とか for とかを書くのがあなたの目的ですか? 何か目的があって if とか for とかを書くはずです。 そして、目的達成のために、思ったことを思った通りに書ける文法があれば、そちらの方が楽なはずです。
この「How より What」は C# 3.0 の新機能群のテーマです。
概念が多いから大変というのは確かにその通りです。 ですが、現在は、そもそも What(やりたいこと)が非常に多いということを忘れてはいけません。
そして、学習が楽だからという理由で、 「覚えることも少ない代わり、出来ることも少ない言語」は作ってはいけません。 それは、学習用言語と実用言語の間に越えづらいギャップを生むだけです。
ギャップを越えなければいけない段階になって、 1から勉強しなおせますか? 逆に、ギャップを越えなかったとして、いつまでも初心者扱いされたいですか?
このギャップ問題は、 90年代に、VB(6.0 以前)と C++ の間で実際に起こっていた問題です。 C# や .NET Framework が作られた目的の1つは、このようなギャップの解消です。
この What(何をしたいか)に基づく難易度でいうと、 学習の大変さに占めるプログラミング言語文法の割合なんて大したことはなくて、 ほとんどがライブラリとかフレームワークの方に依存します。
文法なんて、覚えることも少なければ、ここ数十年での進歩の度合いも大したことはないです。 C# みたいに、たった10年でずいぶんと進歩した言語でみても、フレームワークやライブラリの方の追加・進歩と比べると大したことはありません。
そこで重要なのは、共通型システムです。 異なるプログラミング言語から、同じフレームワーク・ライブラリが使えるというのは非常に重要です。
.NET Framework は共通型システムを備えていて、 C# でも VB(7.0 以降)でも、 .NET Framework に対応した言語ならば何からでも同じクラスが使えます。
目的ベースで考えると、ガベージ コレクションによるメモリの自動管理も非常に大切です。 通常、メモリ管理はプログラムの主たる目的では無いはずです。目的外の事に多大な労力を割いてはいけません。
もちろん、.NET Framework はガベージ コレクションによるメモリ自動管理を備えています。
ただし、システム全体の数%、パフォーマンス的な理由から、自前でメモリ管理することに意味がある部分があります。 自動管理に頼らないネイティブなコードが不要にはならないこと、そしてその需要は全体の数%であることは気に留めておいてください。
そもそも What(何をしたいか)が複雑なため、その分 How(どう書くか)が難しくなるのは多少、やむを得ないものです。
ただし、「出来ることの割に難しい」という理不尽な難しさも当然あります。 もちろん、好き好んで理不尽にしようと考える人は通常はいないわけで、何らかの不手際によって理不尽が生まれます。
問題の多くは、十分な時間と人数を割いて、実際に使ってみて初めて見つかります。 この辺り、大きな企業が主体となって作っている言語は有利です。
C# の場合、公に発表する前に2年ほど時間を書けて作り、 マイクロソフト社内で実際に使い、こなれたところで公開。 その後、正式版のリリースまでさらに2年かけています※1。
※1 旧バージョンの Visual C++ 6.0 や Visual Basic 6.0 が1998年、C# の発表(プレビュー版公開)が2000年。 その後、正式リリース(Visual Studio .NET 2002 製品版の発売)が2002年。
歴史的な事情から、問題が分かっていてもそのまま残さざるを得ないこともあります。
互換性※2を捨ててまでプログラミング言語を修正するのは容易ではないです。 特に、10年以上の期間で使われ続けるような大規模システムの構築に使われる言語では、それはまず不可能です。
そこで、新機能の追加には慎重にならざるを得ません。
C# の場合、機能追加の要望は常に、世界中からかなりの声が寄せられるようですが、 実際に追加するかどうかはかなり慎重に議論して決め、 かなり慎重に実装しているようです※3。
※2 C# には、後方互換性を壊す(古いバージョンのコードが新しいバージョンでコンパイルできなくなる)ような修正は極めて少ないです。 単に、現在の互換性だけでなく、 将来的に互換性の維持を妨げることがないかまで考えて実装しているそうです。
新しいキーワードを導入(後方互換性を壊す危険性が大きい)する場合、 すべて文脈依存キーワード(contextual keyword: 特定の文脈化でのみキーワードとして解釈されて、元々その単語を識別子として使っていたプログラムがそのまま動くようにする)にしてあります。
※3 C# 3.0 のような大きな改修も、 複雑な目的(データ処理の簡素化 = LINQ)の実現に際して、 要件を分析して、1つ1つはシンプルで汎用的な小さな仕様に分解して、個別に実装しています。
C# にも、C++ や Java を参考にし過ぎた(歴史的背景に由来するいまいちな仕様)や、 後から思えばあまり良くなかったと思う機能も皆無ではありませんが、 十分少なく思います。
ちなみに、私見ですが、あまり良くなかった/歴史的に仕方がないけど嫌だと思う部分は以下のようなものです。
- switch ステートメントはもう少し高機能にしても良かった
- 匿名メソッド (後から追加されたラムダ式で全く同じことができ、しかもラムダ式の方が簡素、かつ、高機能)
- Null 許容/非許容の扱い(参照型と値型の概念とは独立して、Null 非許容な参照型とかも作れるようにすべきだった)
- void の扱い(void なものに対しても return できた方が簡素に書ける場面もある)
- Event-based Asynchronous Pattern(今後、別のパターンで置き換えられる予定。でも、後方互換性のために消せない)
- 非ジェネリックなコレクション(ジェネリックスが導入された今となっては完全に不要)
前述の、What(目的)ベースの機能充実や、理不尽の少なさ以外にも、 いくつか、簡単さ/難しさを決める要因が。
ライブラリやフレームワークの挙動まで含めれば、プログラミング言語のすべてを覚えきれる人なんてほとんどいないでしょう。 ソフトウェアに求められる要望も多ければ、言語やライブラリが持つ機能も膨大な今、 大事なのは、「覚える」ことよりも「探す」ことです。 プログラミング言語には検索性の良さが求められます。
膨大な数の機能を、名前空間(パッケージ)やクラスで分類・整理できた方が「簡単」だと思います。
「書くのが楽だから」とか「クラスって概念を教えなくてもいいから」という理由で標準ライブラリをグローバル関数だらけにしてしまうと、 検索性で非常に困ることになります。 当然、1つのクラスにあまりに多くのメソッドを持たせてもいけません。
.NET Framework の標準ライブラリは、1つの名前空間やクラスの中のメンバーが多くなり過ぎないよう、注意を払っています。
Visual Studio などの IDE(Integrated Developement Environment: 統合開発環境)は、コードの補完機能を備えています。 入力可能な変数名や、型、メソッドの候補一覧を出してくれるわけですが、 この時、型やメソッドの一覧は検索目次として使えます。
この「検索目次」としての側面があるため、 IDE は生産性ツールとして優秀であると同時に、 学習ツールとしても優秀です。
そして、C# は、Visual Studio の支援を最大限受けやすいように最初から考えて作られています。
今のご時世、新しいことを調べるにはまず、検索エンジンで検索です。 検索エンジンでの検索性でいうと、記号をやたらと使う言語は不利になります。
プログラマー的な視点で見ると、数式も一種のソースコードなわけですが、 数式は手書き/紙に書くことを前提に最適化されていて、検索性は最悪です。 数式を意識した構文の多い、 プログラマーよりも数学者に向けて作られた言語の場合、記号が多く、検索しづらくなりがちです。
C# は、ラムダ式など、多少、検索に困る構文も持っています。 というか、C# という名前自体が検索に困るのが少し残念です。
暗黙的な型変換などを通して、いつの間にか型が変わってしまうと、検索で困ることが多いです。 実際に使われていて、動いているのに、いくら検索してもそのメソッドの説明が見つからないという事態を起こします。
C# や .NET Framework の標準ライブラリでは、暗黙的に変換を行うような作りになっているものは少ないです。
人間は、新しいものを学ぶ際、既存の知識との類推を求めがちです。 そして、一貫性は類推を助け、学習を容易にします。
一貫性の欠如、例えば、「こっちではこう書くのに、あっちでは同じように書けない」みたいなものがあると、 覚えるのが難しいと感じるはずです。
1つのプログラミング言語内では、文法は一貫しているはずで、 もしそうでない部分があるならば、前述の「理不尽」(歴史的背景や試験運用の不十分)にあたるはずです。
そう簡単に追加ができない文法とは異なり、誰でも追加が出来るライブラリには注意が必要です。
このことを考えると、標準ライブラリがしっかりしているというのが大切です。 下手なライブラリの乱立が減るというのもありますが、 標準ライブラリに合わせる人が増えることで一貫性が増します。
その点、.NET Framework は標準ライブラリがかなりしっかりしています。 標準ライブラリだけでもかなりのことが可能です。 また、サードパーティ製のライブラリも標準に合わせるものが多いです。
異なるプログラミング言語から全く同じライブラリを呼べる。 「同じである」以上の一貫性は無いでしょう。 異なるプログラミング言語から同じものを参照できる、共通型システムは非常に重要です。
前述の通り、.NET Frameworkは共通型システムを持っています。
「短期的に見ると簡単なんだけど、長期的には負債にしかならない」という罠も多いので注意が必要です。
アクセサー(setメソッド/getメソッド)を書くよりも、フィールドを public にする方が、その場限りには楽ですが、 長期的には問題をはらみます。 この点、C# ではアクセサーを簡単に書けるようプロパティという構文を持っています。
オプション引数は、オーバーロードを書く必要が減り、楽は楽ですが、 バージョニングの問題を抱えています。 そのため、C# では長らく、オプション引数の導入を渋っていました(参考: 「余談: なんでいまさら?」 )。
1つのメソッド内に処理を詰め込むのは、書いている最中は楽ですが、後々保守しきれなくなります。 この点、C# は処理を小分けにするための構文、例えば LINQ など(参考: 「データ処理の直交化と汎用化」)を持っていたりします。 また、Visual Studio はリファクタリング機能を持っていて、例えば、詰め込み過ぎた処理をメソッドとして切り出して整理する機能(参考: 「関数」 )があったりします。
概念も難しくない、書くのも簡単。でも、絶対はまるなんてものもあるので注意してください。
例えば非同期処理、特に同時実行制御(ロックとか)なんかがその最たるものです。 同時実行制御用のコードは意外と単純に見えますが、あくまで、コード上、見かけが簡単なだけです。
見かけが簡単なのに何が難しいかというと、テストです。 非同期処理では、100万回に1回しかでないバグとかざらで、テストが非常に難しいです。
チェックインのたびに100万回テスト走らせますか? そして、100万回で安心できますか? 確率論的に「100万回に1回」なとき、1,000万回走らせても出ないときは出ないのです。
こういう難しさは、構文糖衣やライブラリを使って可能な限り隠ぺいすべきです。 (ライブラリ実装者だけが細心の注意を払ってテストを行う。利用者に意識させない。)
C# の持つ多彩な文法には、こういう難しさの隠ぺいが目的のものもあります。
プログラミングにおいて、コードは書いている時間よりも読んでいる時間の方が長いです。 過去に書いたコードの保守、 同僚のコードのレビュー、 検索して見つけたコードを眺める、 等々。
過去に書いたコードに関して、 数か月前の自分すらも赤の他人とほぼ同様である事は肝に銘じておいてください。 記憶はどんどん忘れられて行くものです。
読みやすさを得るためには、例えば以下のような点に注意します。
C# や .NET Framework はこのような注意を払って作られています。