Open12
C# (もう)やめたほうがいい事メモ
ピン留めされたアイテム
はじめに
- C#は互換性を強く意識した言語・処理系
- なので20年前のコードもコピペでそのまま動く
- 互換性高いことによる問題がある
- 知識がアップデートされない
- 古い書き方をずっとやり続ける
- 新しい記法はパフォーマンスも良くなるように設計されてることが多い
参考
csc.exeを直接たたく
-
今はコマンドラインは dotnet cli を使う
- コンパイラを直接叩くことはもうしない
-
そもそも今のC#はマルチプラットフォームなのでWindows限定csc.exe指定は良くない
-
例外
- コンパイラを直接制御するツールとかを作るとき(ただし、Windows限定になる)
- Winodows環境でSDKを入れられない環境でC#を使いたいとき
".NET Framework"という用語を使う
.NETのSDK/Runtimeは無印の「.NET」が最新。
2023-07-31時点は.NET 7が最新。
流れ:
- 元々公式実装の「.NET Framework」、第3者実装の「mono」があった。
- mono開発のXamarin社をMSが買収、新しいOSSの「.NET Core」が生まれた
- 乱立していたのでver.5から「.NET」に統一された(中身は.NET Coreの進化系)
- そのため、まだサポートされているが「.NET Framework」や「mono」は古いSDK/Runtimeになる
- .NETのSDK/Runtimeは「.NET」と書くのが現時点では正しい
- 「.NET Framework」や「.NET Core」の用語を使っているのは 古い記事の目印
不要なおまじないを書く
- 今のC#(最新はC#11)は1行Hello world
Console.WriteLine("Hello World");
-
次のおまじないは省略できる
usingnamespaceclass-
Main関数
-
1ファイルで済むスクリプト的な運用ができる
-
例外
- 規模が大きい場合は意味があるので以下は省略しないほうがよい
namespaceclass-
Main関数
- ※
usingは大規模でも省略に意味がある=コード量が減る
- 規模が大きい場合は意味があるので以下は省略しないほうがよい
string.Format()を使う
- 今のC#は 補完文字列(interpolated string) を使う
var h = "hello";
var w = "world";
var msg = $"{h}, {w}";
- .NET 6以降、コンパイル結果が高速化されている
- 実用面、速度面の両面で今のC#で
string.Format()を使うメリットがない
- 実用面、速度面の両面で今のC#で
- 例外
- C/C++のコードを機械的に移植する時に使うかも?
String.Substring()を使う
- 今のC#は インデクサに範囲指定して抜き出せる
var s = "abcdef";
var s1 = s.Substring(0, 4); //abcd
var s2 = s[0..4]; //abcd
- 更に、
Span<T>化するとより高速化される- 特に何度も抜き出す処理を行う場合
var s3 = s.AsSpan(0, 4).ToString();
var s4 = s.AsSpan()[0..4].ToString();
- 戻り値は
stringではなくReadOnlySpan<char>なのに注意
パフォーマンス計測にSystem.Diagnostics.Stopwatchを使う
代わりにBenchmark.NETを使う。
-
Stopwatch
- ミリ秒まで
- 繰り返し処理など全部自分で書く必要がある
- 処理速度しか記録してくれない
-
Benchmark.NET
- 簡単に書ける
- 平均、標準誤差、標準偏差、アロケーションなどを出してくれる
- パフォーマンス比較で標準的に使われている
-
例外
- Benchmark.NETを導入できない場合
namespaceブロック
- 今の
namespaceはブロックじゃなくて1行で書ける- 全コードのインデントが一つ減る! これはでかい!
namespace Example;
「やたらインデントが多いC#」は過去のもの。
- 例外
- 1ファイル内でたくさん名前空間を制限したい時
- そんなことある?
- 名前空間の入れ子をやりたい時
- でも
namespace A.B.C;って書けるしなぁ… いる?
- でも
- 1ファイル内でたくさん名前空間を制限したい時
nullの判定に==を使う (obj == null)
is演算子やis not演算子を使う
if(obj is null){
//...
}
- 比較(
==)のはずなのに代入(=)してた…っていうミスを防げる- もちろん色々他のエラーや警告で気が付けるけど
- 演算子オーバーロードされてても判定できる
nullチェックの時は?.や??や??=を使う
var result = obj ?? new Something();
//古い書き方
Something result;
if(obj == null){
result = new Something();
}
-
nullチェック自体が簡単に書けるのでコードの見通しが良くなる
-
if(obj == null)が出てきたら2重の意味で怪しいと思うべき
逐語的文字列リテラルを使う @"..."
- 生文字列リテラルを使う
GoFパターンで置き換えを考慮すべきもの
GoFパターンのうち以下はもう使われない or 既にあるものを利用するようになっている
-
Singletonパターン -> DIのライフタイム(例:
AddSingleton())- 解説:モダン環境では DI/ライフタイム管理を使うのが常識になってきており、古典的なシングルトン実装を “設計パターンだからとりあえず使う” のはむしろアンチパターンになる
-
Abstract Factoryパターン -> DI + ジェネリック
- 解説:抽象ファクトリー自体は今でも有効なパターンですが、フレームワークが “生成と切り替え” をサポートしてくれているケースが増えているため、「古典的な抽象ファクトリーを使おう」と飛びつく前に、フレームワークが提供する手段(DI、コンテナ、ジェネリクス)で対応できないかをまず考えるべき
-
Obververパターン ->
IObservable<T>/IObserver<T>/ Rxなど- 解説:通知・イベント/リアクティブな設計を要する場面では、いまや言語・ライブラリが強力なサポートを提供しています。したがって、古典的な「Subject/Observer」クラス階層を構築するより、既存フレームワークの機能を利用する方が保守性・拡張性共に優れるケースが多い
-
Adapterパターン
- 解説:ライブラリ設計やDIを前提としたモダン設計では「抽象インターフェース+実装クラス」という構成が普通になっており、アダプターパターンを飛び越えて最初から拡張可能な構造を作る方が良い
以下は言語仕様として存在する
- Value Objectパターン ->
record- 値の同一性・不変性を自動サポート。EqualsやGetHashCodeを自作する必要がない。
- Strategyパターンの一部 -> パターンマッチング
- 条件分岐を switch 式で簡潔に書け、戦略切替をクラス分割せず表現可能。
- Factoryパターン -> プライマリーコンストラクタ
- 依存注入を直接コンストラクタに組み込め、冗長なコードを削減。
- FactoryやBuilderのボイラープレート -> Source Generator
- コンパイル時コード生成で、パターンの定型部分を自動化
キャスト (T) / as T
-
(T)のキャスト- キャストに失敗するとexceptionをthrowする
- なので try-catch で毎回括らないと危険 -> 冗長!
-
as Tのキャスト- キャストに失敗するとnullになる
- なので毎回nullチェックが必要 -> 冗長!
-
対策
- 型パターンを使う。失敗してもexceptionを吐かず、デフォルトでnullチェックしてくれる
var t1 = new T1();
//T2にキャストできなければ抜ける
if(t1 is not T2 t2){ return; }
//t2はnot null
キャストは辛いよ
//✖:失敗するとexceptionをthrow
T2 t2 = (T1)t1;
//毎回これかくの…?
try
{
T2 t2 = (T1)t1;
}
catch (Exception ex)
{ /* ... */ }
//✖:失敗するとnullになる
var t2 = t1 as T2;
//必ずnullチェックしないといけない
if(t2 is null){ return; }