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");
  • 次のおまじないは省略できる

    • using
    • namespace
    • class
    • Main関数
  • 1ファイルで済むスクリプト的な運用ができる

  • 例外

    • 規模が大きい場合は意味があるので以下は省略しないほうがよい
      • namespace
      • class
      • Main関数
    • usingは大規模でも省略に意味がある=コード量が減る
いぬいぬいぬいぬ

string.Format()を使う

  • 今のC#は 補完文字列(interpolated string) を使う
var h = "hello";
var w = "world";
var msg = $"{h}, {w}";
  • .NET 6以降、コンパイル結果が高速化されている
    • 実用面、速度面の両面で今のC#でstring.Format()を使うメリットがない

https://ufcpp.net/study/csharp/start/improvedinterpolatedstring/

  • 例外
    • C/C++のコードを機械的に移植する時に使うかも?
いぬいぬいぬいぬ

String.Substring()を使う

  • 今のC#は インデクサに範囲指定して抜き出せる
var s = "abcdef";
var s1 = s.Substring(0, 4); //abcd
var s2 = s[0..4]; //abcd

https://qiita.com/YouKnow/items/93b4b978ecb113616fae

  • 更に、Span<T>化するとより高速化される
    • 特に何度も抜き出す処理を行う場合
var s3 = s.AsSpan(0, 4).ToString(); 
var s4 = s.AsSpan()[0..4].ToString();
  • 戻り値はstringではなくReadOnlySpan<char>なのに注意
いぬいぬいぬいぬ

パフォーマンス計測にSystem.Diagnostics.Stopwatchを使う

代わりにBenchmark.NETを使う。

https://github.com/dotnet/BenchmarkDotNet

  • Stopwatch

    • ミリ秒まで
    • 繰り返し処理など全部自分で書く必要がある
    • 処理速度しか記録してくれない
  • Benchmark.NET

    • 簡単に書ける
    • 平均、標準誤差、標準偏差、アロケーションなどを出してくれる
    • パフォーマンス比較で標準的に使われている
  • 例外

    • Benchmark.NETを導入できない場合
いぬいぬいぬいぬ

namespaceブロック

  • 今のnamespaceはブロックじゃなくて1行で書ける
    • 全コードのインデントが一つ減る! これはでかい!
namespace Example;

「やたらインデントが多いC#」は過去のもの。

  • 例外
    • 1ファイル内でたくさん名前空間を制限したい時
      • そんなことある?
    • 名前空間の入れ子をやりたい時
      • でも namespace A.B.C; って書けるしなぁ… いる?
いぬいぬいぬいぬ

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
    • コンパイル時コード生成で、パターンの定型部分を自動化

https://medium.com/@vahidbakhtiaryinfo/modern-c-design-patterns-you-should-actually-use-in-2025-32dd41df38f9

いぬいぬいぬいぬ

キャスト (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; }