コード記述スタイルと可読性に関する議論 - C#

コード記述スタイルと可読性に関する議論について紹介します。

概要

C#はバージョンアップに伴い、新しい文法や記述方法が追加されてきました。 特に新しいコード記述方式では、以前のプログラミング言語では利用できない書式もあり、利用に賛否両論があるものもあります。 この記事では、C#のコード記述方式に関する議論やコメントを紹介します。

チームプログラミングで、オールドスタイルなプロフラマーとコードスタイルで揉めるといった話もあります。

三項演算子

はじめに以前から賛否両論がある三項演算子について紹介します。
次のコードを記述します。
    private void button1_Click(object sender, EventArgs e)
    {
      Random rnd = new Random();
      int value = rnd.Next(10);
      string output = value < 5 ? "small" : "large";
      textBox1.Text += output;
    }

ボタンをクリックすると、乱数を生成し、乱数の値が5より小さければ、テキストボックスに"small"の文字列を表示します。 乱数の値が5以上の場合は、"large"の文字列をテキストボックスに表示します。

このコードで議論の対象になるのは以下のコードです。Cの時代からある三項演算子と呼ばれる記述ですが、直感的でないため可読性が悪いといわれ、 よく議論されてきました。C言語になじみのないプログラマには意味が分からないとの指摘があります。

  string output = value < 5 ? "small" : "large";

書式は次の通りです。条件文が trueの場合は、2項目の値が戻り、falseの場合は3項目の値が戻ります。
三項演算子の動作はこちらの記事を参照して下さい。
(条件文) ? (条件文がtrueの時の値) : (条件文がfalseの時の値) 

if文で置き換えることが多く、if文で表現すると次のコードになります。
  string output;
  if (value < 5) output = "small"; else output = "large";

今回紹介した短い表現では理解しやすいですが、それぞれの項が長い記述になると、さらに理解が難しくなる傾向にあります。

foreach

foreach文はC#の登場時から利用できる構文で20年近く利用できる構文ですが、C言語にはなく、 .NET以前のVBやBASICにも無い構文のため、古いスタイルのプログラマには抵抗感のある方もいます。 古いプログラミング言語では、for文でループ変数を伴うループか、while文によるループのどちらかしかなかったためです。

以下のforeachコードを例にします。
    private void button3_Click(object sender, EventArgs e)
    {
      int[] data = new int[5];
      data[0] = 6;
      data[1] = 4;
      data[2] = 2;
      data[3] = 0;
      data[4] = -2;

      foreach (int d in data) {
        textBox1.Text += d.ToString() + "\r\n";
      }
    }

ただし極度のクラシックスタイルのプログラマーでは上記のコードが許容できず以下のコードでないと受け付けられない場合があります。
オールドスタイルなプログラマーのコメントであるのは、foreach でループで選択される値がi=0から順番に選択されないように見えるので気持ち悪いといったものがあります。 (foreachでリストや配列をループする場合に順番が不定になるのではないかという疑念。実際は配列やリストの先頭からループします。)
なお、ループ内でループ変数(下記コードの変数i)が変化する場合やループ変数の判定がある場合はforeachは使えません。
    private void button3_Click(object sender, EventArgs e)
    {
      int[] data = new int[5];
      data[0] = 6;
      data[1] = 4;
      data[2] = 2;
      data[3] = 0;
      data[4] = -2;

      for (int i = 0; i < data.Length; i++) {
        textBox1.Text += data[i].ToString() + "\r\n";
      }
    }

ジェネリック

ジェネリックは登場してから15年以上が経過しており、現在では拒否感を感じるプログラマーはほとんどいないと思われますが、 標準のC言語には無いことや、初期のC# 1.0では利用できなかったため、登場直後は議論になったこともあります。

以下のジェネリック を利用したコードを例にします。
    private void button2_Click(object sender, EventArgs e)
    {
      List<int> data = new List<int>();
      data.Add(4);
      data.Add(6);
      data.Add(16);

      foreach (int d in data) {
        textBox1.Text += d.ToString()+"\r\n" ;
      }
    }

ジェネリック の登場以前は以下の記述をしていました。
ArrayListはObject型のため、どんな値でも挿入できてしまうため、 コードの堅牢さを考えてもジェネリック型を使用するのがベストです。
古いスタイルのプログラマーでは<T>に若干の気持ち悪さを感じるかもしれませんが、実際に問題ありだという発言をしているシーンを見たことはないです。
    private void button2_Click(object sender, EventArgs e)
    {
      ArrayList data = new ArrayList();
      data.Add(4);
      data.Add(6);
      data.Add(16);

      foreach (int d in data) {
        textBox1.Text += d.ToString() + "\r\n";
      }
    }

匿名関数

以下のコードを準備します。
リストを値の降順でソートする処理です。
Sortメソッド内のパラメーターのコードが匿名関数です。JavaScriptでも多用されている記法でもあり、最近ではなじみのある書式になっていますが、 もともと関数ポインタを渡す構造のため、登場時には違和感を感じるプログラマーも多かったコードです。
現在では匿名関数NGなスタイルは少ないかと思います。
ただし、可読性は若干落ちるかなという印象はあります。一方で他の場所での再利用が無いことが保証されているのはメリットかと思われます。
    private void button4_Click(object sender, EventArgs e)
    {
      List<int> data = new List<int>() { 4, 10, 3, 5, 8, 7, 2, 9 };

      data.Sort(delegate (int x, int y) {
        if (x == y) {
          return 0;
        }
        else if (x < y) {
          return 1;
        }
        else {
          return -1;
        }
      });

      foreach (int d in data) {
        textBox1.Text += d.ToString() + "\r\n";
      }
    }

クラシックなスタイルで記述する場合は、デリゲートで与えるメソッドは別のメソッドとして記述し、メソッド名をSortメソッドの引数に与えるコードになります。 以下のコードになります。
    private void button5_Click(object sender, EventArgs e)
    {
      List<int> data = new List<int>() { 4, 10, 3, 5, 8, 7, 2, 9 };
      data.Sort(customProc);
      foreach (int d in data) {
        textBox1.Text += d.ToString() + "\r\n";
      }
    }

    private int customProc(int x, int y)
    {
      if (x == y) {
        return 0;
      }
      else if (x < y) {
        return 1;
      }
      else {
        return -1;
      }
    }

ラムダ式

ラムダ式も登場して15年以上経過しているため、最近では抵抗感が少なくなっていますが、 これまでに紹介したジェネリックや匿名関数より可読性が低くなるため、いまだに抵抗感のあるプログラマーもいます。
次のコードを準備します。

リストを値の降順でソートする処理です。
ラムダ式に加えてネストしている三項演算子もあり、人によっては抵抗感のあるコードになっています。
  private void button6_Click(object sender, EventArgs e)
  {
    List<int> data = new List<int>() { 4, 10, 3, 5, 8, 7, 2, 9 };
    data.Sort((int x, int y) => x == y ? 0 : (x < y ? 1 : -1));
    foreach (int d in data) {
      textBox1.Text += d.ToString() + "\r\n";
    }
  }

三項演算子を使わない記述にすると以下になります。こちらの書式であれば匿名メソッドに抵抗感がなければ受け入れやすい記述です。
  private void button6_Click(object sender, EventArgs e)
    {
      List<int> data = new List<int>() { 4, 10, 3, 5, 8, 7, 2, 9 };
      data.Sort((int x, int y)=>{
        if (x == y) {
          return 0;
        }
        else if (x < y) {
          return 1;
        }
        else {
          return -1;
        }
      });

      foreach (int d in data) {
        textBox1.Text += d.ToString() + "\r\n";
      }
    }

型推論

こちらも、賛否が分かれる記述です。次のコードを用意します。
C#でこの記述を見ると、クラシックなプログラマーだともやもやする方もそれなりにいるかと思います。
「BASICや旧VBじゃないんだよ!」というコメントもありそうです。一方でJavaScriptのコードを書き慣れている場合には違和感を感じないと思います。
もともとC系の言語は明確な型宣言をするため、どのような値でも代入可能な変数には、オールドスタイルなプログラマーは違和感を感じます。
    private void button7_Click(object sender, EventArgs e)
    {
      var a = 4;
      var b = 8;
      var c = a + b;
      textBox1.Text += c;
      textBox1.Text += "\r\n";
      var x = "Penguin";
      var y = "Cookie";
      var z = x + y;
      textBox1.Text += z;
    }

型推論を使わない場合は以下のコードになります。
    private void button7_Click(object sender, EventArgs e)
    {
      int a = 4;
      int b = 8;
      int c = a + b;
      textBox1.Text += c;
      textBox1.Text += "\r\n";
      string x = "Penguin";
      string y = "Cookie";
      string z = x + y;
      textBox1.Text += z;
    }

NULL許容型 (Nullable)

int? char? といった記述でnullを許容する型を定義できます。値の実体が代入できる変数に対してnullを許容するのは抵抗感があるというプログラマーもいます。 一方でポインタだと思えばすんなり受け入れられる。というコメントもあります。
NULL許容型の詳細はこちらの記事を参照してください。

LINQ

賛否が分かれる記法の一つにLINQがあります。
従来のプログラムには無い記述方法のため、最近でも抵抗感が強いコードです。

特にSQL記法では、オールドスタイルなプログラマーでは「DB屋とプログラマーは違うんだよ」というコメントも出そうでかなり抵抗感のあるコードの印象です。
シンプルなコードでは可読性もあまり落ちませんが、SQL文の条件式が長くなると、可読性が落ちる場合もあります。
また、SQL文形式でも記述でき、WhereメソッドやSelectメソッドを利用した記述もできるため、同じ内容の処理でも複数の記述方法ができてしまうこともデメリットです。

さらに、メソッド形式でのLINQの場合はラムダ式を利用した記述が一般的なため、新しい記法が2つ導入されており、慣れていないと可読性の面でも苦労します。
また、LINQのメソッド記述方法では「メソッドチェーン」と呼ばれる、戻り値オブジェクトに対してメソッドを呼び出す記述スタイルを多用することがあり、こちらも賛否が分かれるスタイルです。

以下のコードを用意します。5以上の項目を取得するコードです。
    private void button8_Click(object sender, EventArgs e)
    {
      List<int> data = new List<int> { 10, 2, 4, 1, 9, 3, 8, 12, 15, 5 };
      //var query = from i in data where i > 5 select i;
      IEnumerable<int> query = from i in data where i > 5 select i;

      foreach (int n in query) {
        textBox1.Text += string.Format("{0:d}\r\n", n);
      }
    }

SQL形式でない記述方法の場合は下記になります。
    private void button8_Click(object sender, EventArgs e)
    {
      List<int> data = new List<int> { 10, 2, 4, 1, 9, 3, 8, 12, 15, 5 };
      //var query = data.Where((int i) => i > 5);
      IEnumerable<int> query = data.Where((int i) => i > 5);

      foreach (int n in query) {
        textBox1.Text += string.Format("{0:d}\r\n", n);
      }
    }

LINQを使わない記法にする場合は以下のコードに書き換えられます。LINQを利用することでループ処理を記述しなくても済みます。
    private void button9_Click(object sender, EventArgs e)
    {
      List<int> data = new List<int> { 10, 2, 4, 1, 9, 3, 8, 12, 15, 5 };

      List<int> query = new List<int>();
      foreach (int n in data) {
        if (n>5) query.Add(n);
      }

      foreach (int n in query) {
        textBox1.Text += string.Format("{0:d}\r\n", n);
      }
    }

SQL記法で select句に処理が入る場合のコード例です。dataの各値を2倍する処理になります。
    private void button10_Click(object sender, EventArgs e)
    {
      List<int> data = new List<int> { 8,7,2,4 };
      IEnumerable<int> query = from i in data  select i*2;
      foreach (int n in query) {
        textBox1.Text += string.Format("{0:d}\r\n", n);
      }
    }

Selectメソッドを利用する例です。
    private void button10_Click(object sender, EventArgs e)
    {
      List<int> data = new List<int> { 8,7,2,4 };
      IEnumerable<int> query = data.Select((int i) => i*2);
      foreach (int n in query) {
        textBox1.Text += string.Format("{0:d}\r\n", n);
      }
    }

並び替えを実行する例です。SQL文での記述方法では orderby 句を利用します。
    private void button11_Click(object sender, EventArgs e)
    {
      List<int> data = new List<int> { 8, 7, 2, 4 };
      IEnumerable<int> query = from i in data orderby i select i * 2;
      //IEnumerable<int> query = from i in data orderby i descending select i * 2; //逆順の場合
      foreach (int n in query) {
        textBox1.Text += string.Format("{0:d}\r\n", n);
      }
    }

メソッド形式での記法は以下になります。いわゆる「メソッドチェーン」の記述スタイルになり、 この記法に抵抗感のあるオールドスタイルなプログラマーも結構いるかと思われます。
    private void button11_Click(object sender, EventArgs e)
    {
      List<int> data = new List<int> { 8, 7, 2, 4 };
      IEnumerable<int> query = data.OrderBy((int i)=> i).Select((int i) => i*2);
      //IEnumerable<int> query = data.OrderByDescending((int i) => i).Select((int i) => i * 2); //逆順の場合

      foreach (int n in query) {
        textBox1.Text += string.Format("{0:d}\r\n", n);
      }
    }

更に型推論が入り、varで宣言されると、イラっとくる方もいるかもしないです。
    private void button13_Click(object sender, EventArgs e)
    {
      var data = new List<int> { 8, 7, 2, 4 };
      var query = data.OrderBy((int i) => i).Select((int i) => i * 2);

      foreach (int n in query) {
        textBox1.Text += string.Format("{0:d}\r\n", n);
      }
    }

Func<T>, Action<T>

Func<T>Action<T> 自体には抵抗感はないものの、組み合わせると拒否感が出るパターンがあります。

以下のコードを用意します。4の個数を数える処理ですが、直接Get4Count()を呼び出せば良く、Func<T>を使う意味が全く無いコードです。
    private void button15_Click(object sender, EventArgs e)
    {
      Func<List<int>, int> fu = Get4Count;
      List<int> inputdata = new List<int>() { 5, 7, 4, 2, 3, 4, 2 };
      int cnt = fu(inputdata);
      textBox1.Text += string.Format("{0:d}\r\n", cnt);
    }

    private int Get4Count(List<int> data)
    {
      int result = 0;
      foreach (int i in data) {
        if (i == 4) result++;
      }
      return result;
    }

Func<T>登場時は、戻り値の型が最後のTであることが許せない、というコメントもあり、 以下のコードのように、delegateで記述すべきだ、という指摘もありましたが、 登場から年月が経ち、Func<T>に慣れたこともあり、最近はあまり指摘されないかと思います。
(それでも、Func<T1, T2, T3..., Tn>の最後のTnが戻り値の型という部分に若干引っかかる方はいるのではと思います。)
  delegate int MyFunc(List<int> data);

  private void button16_Click(object sender, EventArgs e)
  {
    MyFunc fu = Get4Count;
    List<int> inputdata = new List<int>() { 5, 7, 4, 2, 3, 4, 2 };
    int cnt = fu(inputdata);
    textBox1.Text += string.Format("{0:d}\r\n", cnt);
  }

  private int Get4Count(List<int> data)
  {
    int result = 0;
    foreach (int i in data) {
      if (i == 4) result++;
    }
    return result;
  }

しかし、Func<T>は多くの場合、匿名関数とセットで使われ、さらに匿名関数はラムダ式で記述されます。
以下のコードになります。
    private void button14_Click(object sender, EventArgs e)
    {
      Func<List<int>, int> fu = delegate(List<int>data){
        int result = 0;
        foreach (int i in data) {
          if (i == 4) result++;
        }
        return result;
      };

      List<int> inputdata = new List<int>() { 5, 7, 4, 2, 3, 4, 2 };
      int cnt = fu(inputdata);
      textBox1.Text += string.Format("{0:d}\r\n", cnt);
    }

ラムダ式が登場したことで匿名関数をdelegateで記述するケースは減り、多くの場合は次のコードになります。
    private void button17_Click(object sender, EventArgs e)
    {
      Func<List<int>, int> fu = (List<int> data) => {
        int result = 0;
        foreach (int i in data) {
          if (i == 4) result++;
        }
        return result;
      };

      List<int> inputdata = new List<int>() { 5, 4, 4, 2, 3, 4, 2, 9, 4 };
      int cnt = fu(inputdata);
      textBox1.Text += string.Format("{0:d}\r\n", cnt);
    }

さらに、ラムダ式をステートメントではなく式形式で記述したいので、LINQと組み合わせて、次のコードに直します。
ここまでくると、さすがに、イラっとする方もいるのではないでしょうか。4の個数を返す処理ですが、ぱっと見、何をしているのかわかりにくいかもしれません。
スタイルが関数型プログラミング言語の記述に似てくるため、特にLISP嫌いなプログラマーは怒りに打ち震えるかもしれません。

Func<T>の問題ではないのですが、Func<T>があるがゆえに、匿名関数を使い、匿名関数があるためラムダ式を使い、ラムダ式の式形式表現があるため、LINQを使うといった、 芋づる式に新しい書式が次々使われ、結果的に可読性が落ちるケースがあります。

一方で、この書式のほうがロジックがわかりやすいというプログラマーもいるため、賛否両論分かれるところです。
ただし処理的には、4のみのIEnumerable<int>を作成してリストに変換してCountプロパティで個数を取得するため、パフォーマンスは落ちるのではと考えられます。
    private void button17_Click(object sender, EventArgs e)
    { 
      Func<List<int>, int> fu = (List<int> data) => data.Where((int i) => i==4).ToList().Count;
      List<int> inputdata = new List<int>() { 5, 4, 4, 2, 3, 4, 2, 9, 4 };

      int cnt = fu(inputdata);
      textBox1.Text += string.Format("{0:d}\r\n", cnt);
    }

正規表現

RegExクラスを利用した正規表現も賛否が分かれるコードです。C#ではPerlのようにコード内に記述されるケースは少ないものの、複雑な正規表現式は可読性を落とします。
一方で正規表現を利用せずにロジックで文字列処理を組むと処理が複雑になる場合もあり、コードをシンプル化してバグを減らす面でも、 ある程度複雑な文字列処理の場合は正規表現の利用は避けて通れないのが現状かと思われます。

ただし、よくある論争として以下があります。
  • 処理速度を優先するルーチンなので、正規表現は使わずにロジックで実装するべきだ
  • 処理速度はCPUパワーでカバーすればよい、独自ロジックを実装せずに、コードを減らしてバグを減らすべきだ

明らかに単純な文字列処理で正規表現を利用するのはオーバースペックですが、そこそこの処理の場合、正規表現を使うべきか、使わないべきかで論争になることがあります。
一方で、RegExの改善やチューニングは進んでいるため、一文字づつループで舐める処理を独自実装するのであれば、RegExを使ったほうが、コードもシンプルになり、処理効率も良い場合があります。

null合体演算子

????= 演算子の利用です。C#8から採用されたため、まだなじみがなく、利用に抵抗感のある場合があります。
Null合体演算子については以下の記事を参照してください。

関数先頭での変数宣言

かなり古い時代のCでは関数先頭での変数宣言しかできませんでした。また、forループのループ変数宣言も関数の先頭で宣言する必要がありました。
以下のコードを用意します。
    private void button12_Click(object sender, EventArgs e)
    {
      int[] data = new int[5] { 4, 6, 1, 8, 0 };
      int a = data[0] + data[1];
      int b = data[2] - data[1];

      data[4] = a + b;

      for (int i = 0; i < data.Length; i++) {
        textBox1.Text += data[i].ToString() +", ";

      }
    }

関数冒頭での変数宣言が染みついていると、上記のコードは次のコードになります。
(Pascalプログラムスタイルが染みついている場合には、下のコードのように書いてしまう方もいます。)
関数冒頭での宣言はメリット、デメリットありますが、for文のループ変数はfor部分で宣言するのが良いと思います。
    private void button12_Click(object sender, EventArgs e)
    {
      int a;
      int b;
      int[] data;
      int i;

      data = new int[5] { 4, 6, 1, 8, 0 };
      a = data[0] + data[1];
      b = data[2] - data[1];

      data[4] = a + b;

      for (i = 0; i < data.Length; i++) {
        textBox1.Text += data[i].ToString() +", ";

      }
    }

param 引数

メソッドのparamのパラメータを利用すると、配列の引数に対して、複数のパラメーターで値を与えることができます。
詳しくはこちらの記事を参照してください。

paramを利用することで、メソッドの引数の個数が不定になり、抵抗感のあるコードになる場合があります。

DI (Dependency Injection)

DIを利用することで、クラスで必要なオブジェクトを外部から挿入できますが、コンストラクタの引数が不定になり、 引数の順番や個数に関係なく動作してしまうコードに気持ち悪さを感じる場合があります。

詳しくはこちらの記事を参照してください。

リフレクション

リフレクションはプログラミング記述方式とは若干異なりますが、強い違和感を持つプログラマーもいます。 (文字列でインスタンス生成するなんて許せない。など)
リフレクションについてはこちらの記事を参照してください。

著者
iPentec Document 編集部
iPentec Document 編集部です。
快適な生活のための情報、価値のある体験やレビューを提供します。
掲載日: 2022-11-06
iPentec all rights reserverd.