【C#】【book】「実戦で役立つ C# プログラミングのイディオム/定石&パターン」の学習記録(2)

下記の「実戦で役立つ Cプログラミングのイディオム/定石&パターン」を使った学習記録のページです。
本ページのプログラムは「実戦で役立つ Cプログラミングのイディオム/定石&パターン」をもとにして作成したものも多数あります。

2.1 距離換算プログラム

  • フィートをメートルに換算するプログラム

2.1.1 最初のバージョン

Program.cs
using System;

namespace DistanceConverter
{
  class Program
  {
    static void Main( string[] args )
    {
      // フィートからメートルへの対応表を出力する
      for ( int feet = 1 ; feet <= 10 ; feet++ )
      {
        double meter = feet * 0.3048;
        Console.WriteLine( "{0} ft = {1:0.0000} m", feet, meter );
      }
    }
  }
}

 

2.1.2 計算ロジックをメソッドとして独立させる

  • 2.1.1 では Main 関数に変換ロジックを書いていたので FeetToMeter 関数として独立させる
Program.cs
using System;

namespace DistanceConverter
{
  class Program
  {
    static void Main( string[] args )
    {
      // フィートからメートルへの対応表を出力する
      for ( int feet = 1 ; feet <= 10 ; feet++ )
      {
        double meter = FeetToMeter( feet );
        Console.WriteLine( "{0} ft = {1:0.0000} m", feet, meter );
      }
    }

    static double FeetToMeter( int feet )
    {
      return feet * 0.3048;
    }
  }
}

 

2.1.3 プログラムに機能を追加する

2.1.4 メソッドを単機能にする

  • メートル ⇔ フィートへの相互変換を可能にする
    • 「-tom」でフィートからメートルへ変換する
    • 「-tof」でメートルからフィートへ変換する
Program.cs
using System;

namespace DistanceConverter
{
  class Program
  {
    static void Main( string[] args )
    {
      if ( args.Length >= 1 && args[0] == "-tom" )
      {
        PrintFeetToMeterList( 1, 10 );
      }
      else
      {
        PrintMeterToFeetList( 1, 10 );
      }
    }

    // フィートからメートルへの対応表を出力する
    static void PrintFeetToMeterList( int start, int stop )
    {
      for ( int feet = start ; feet <= stop ; feet++ )
      {
        double meter = FeetToMeter( feet );
        Console.WriteLine( "{0} ft = {1:0000} m", feet, meter );
      }
    }

    // メートルからフィートへの対応表を出力する
    static void PrintMeterToFeetList( int start, int stop )
    {
      for ( int meter = 1 ; meter <= 10 ; meter++ )
      {
        double feet = MeterToFeet( meter );
        Console.WriteLine( "{0} m = {1:0.0000} ft", meter, feet );
      }
    }

    // フィートからメートルを求める
    static double FeetToMeter( int feet )
    {
      return feet * 0.3048;
    }

    // メートルからフィートを求める
    static double MeterToFeet( int meter )
    {
      return meter / 0.3048;
    }

  }
}

 

2.1.5 クラスとして分離する

2.1.6 クラスを利用する

  • FeetConverter クラス を作成する
  • FeetConverter クラスには次のメソッドを持たせる
    • メートルからフィートに変換するFromMeter メソッド
    • フィートからメートルに変換するToMeter メソッド
FeetConverter.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace DistanceConverter
{
  class FeetConverter
  {
    // メートルからフィートを求める
    public double FromMeter( double meter )
    {
      return meter / 0.3048;
    }

    // フィートからメートルを求める
    public double ToMeter( double feet )
    {
      return feet * 0.3048;
    }
  }
}

 

Program.cs
using System;

namespace DistanceConverter
{
  class Program
  {
    static void Main( string[] args )
    {
      if ( args.Length >= 1 && args[0] == "-tom" )
      {
        PrintFeetToMeterList( 1, 10 );
      }
      else
      {
        PrintMeterToFeetList( 1, 10 );
      }
    }

    // フィートからメートルへの対応表を出力する
    static void PrintFeetToMeterList( int start, int stop )
    {
      FeetConverter converter = new FeetConverter();
      for ( int feet = start ; feet <= stop ; feet++ )
      {
        double meter = converter.ToMeter( feet );
        Console.WriteLine( "{0} ft = {1:0000} m", feet, meter );
      }
    }

    // メートルからフィートへの対応表を出力する
    static void PrintMeterToFeetList( int start, int stop )
    {
      FeetConverter converter = new FeetConverter();
      for ( int meter = 1 ; meter <= 10 ; meter++ )
      {
        double feet = converter.FromMeter( meter );
        Console.WriteLine( "{0} m = {1:0.0000} ft", meter, feet );
      }
    }
  }
}

 

2.1.7 静的メソッドにする

  • FromMeter および ToMeter メソッドは、インスタンスが必要なメンバ変数(プロパティ)やメソッドを参照していない
  • 従って、static 変数にすることができる
FeetConverter.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace DistanceConverter
{
  class FeetConverter
  {
    // メートルからフィートを求める
    public static double FromMeter( double meter )
    {
      return meter / 0.3048;
    }

    // フィートからメートルを求める
    public static double ToMeter( double feet )
    {
      return feet * 0.3048;
    }

  }
}

 

Program.cs
  • インスタンスを使わずに FeetConverter.XXXX というようにして呼び出すことができる

 

2.1.8 静的クラスにする

2.1.9 定数を定義する

2.1.10 完成したソースコード

  • 2.1.7 の通り FeetConverter のメソッドはすべて static であることからクラス自体を static にすることができる
  • static クラスはインスタンスの生成ができないので注意すること
  • 定数は private にすること
  • もしも公開する場合は static readonly にしておくこと
  public static readonly double Ratio = 0.3048;

 

FeetConverter.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace DistanceConverter
{
  public static class FeetConverter // static クラスにする
  {
    private const double ratio = 0.3048; // 定数にする

    // メートルからフィートを求める
    public static double FromMeter( double meter )
    {
      return meter / ratio;
    }

    // フィートからメートルを求める
    public static double ToMeter( double feet )
    {
      return feet * ratio;
    }

  }
}

 

Program.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace DistanceConverter
{
  public static class FeetConverter // static クラスにする
  {
    private const double ratio = 0.3048; // 定数にする

    // メートルからフィートを求める
    public static double FromMeter( double meter )
    {
      return meter / ratio;
    }

    // フィートからメートルを求める
    public static double ToMeter( double feet )
    {
      return feet * ratio;
    }

  }
}

 

2.2 売り上げ集計プログラム

2.2.5 初版の完成
  • 1カ月の店舗別カテゴリ別の売上金額が、カンマ区切りで記録されているCSV ファイルがある。
  • このファイルを読み込み、金額を集計するコンソールアプリケーションである。
  • C:\Users\neko\cs\Sales\Sales\bin\Debug\netcoreapp2.1\sales.csv として置いた。
sales.csv

 

Sale.cs
  • プロパティのみを持つクラス
using System;
using System.Collections.Generic;
using System.Text;

namespace Sales
{
  // 売り上げクラス
  public class Sale
  {
    // 店舗名
    public string ShopName { get; set; }
    // 商品カテゴリ
    public string ProductCategory { get; set; }
    // 売り上げ高
    public int Amount { get; set; }
  }
}

 

SalesCounter.cs
  • 店舗別売り上げ高を計算するクラス
using System;
using System.Collections.Generic;
using System.Text;

namespace Sales
{
  // 売り上げ集計クラス
  public class SalesCounter
  {
    private List<Sale> _sales;

    // コンストラクタ
    public SalesCounter( List<Sale> sales )
    {
      _sales = sales;
    }

    // 店舗別売り上げを求めるメソッド
    public Dictionary<string, int> GetPerStoreSales()
    {
      Dictionary<string, int> dict = new Dictionary<string, int>();
      foreach ( Sale sale in _sales )
      {
        if ( dict.ContainsKey( sale.ShopName ) )
        {
          dict[sale.ShopName] += sale.Amount;
        }
        else
        {
          dict[sale.ShopName] = sale.Amount;
        }
      }
      return dict;
    }
  }
}

 

Program.cs
using System;
using System.Collections.Generic; // List を使うために必要
using System.IO;

namespace Sales
{
  class Program
  {
    static void Main( string[] args )
    {
      // sales.csv は「C:\Users\neko\cs\Sales\Sales\bin\Debug\netcoreapp2.1\.」に置いた
      SalesCounter sales = new SalesCounter( ReadSales( "sales.csv" ) );
      Dictionary<string, int> amountPerStore = sales.GetPerStoreSales();

      foreach ( KeyValuePair<string, int> obj in amountPerStore )
      {
        Console.WriteLine( "{0} {1}", obj.Key, obj.Value );
      }
    }

    // 売り上げデータを読み込み、Sale オブジェクトのリストを返す
    static List<Sale> ReadSales( string filePath )
    {
      List<Sale> sales = new List<Sale>();
      string[] lines = File.ReadAllLines( filePath ); // ファイルの内容を一度に読み出す

      foreach ( string line in lines ) // 1行ずつ解析する
      {
        string[] items = line.Split( ',' );
        Sale sale = new Sale  // Sale オブジェクトを生成する
        {
          ShopName = items[0],
          ProductCategory = items[1],
          Amount = int.Parse( items[2] )
        };
        sales.Add( sale );    // Sale オブジェクトを List に追加する
      }

      return sales;
    }
  }
}
実行結果

f:id:dnkrnka:20181028213312p:plain
 
 

2.2.6 メソッドの移動

次のようにリファクタリングを行う。

  • Program.cs で定義した ReadSales メソッドを SalesCounter クラスに切り出す
  • Program.cs では SalesCounter オブジェクトを生成して ReadSales メソッドを呼び出す
  • SalesCounter コンストラクタを(ファイルパスを受け取るように)変更する。

 

Sale.cs
  • 初版からの変更は無い。
using System;
using System.Collections.Generic;
using System.Text;

namespace Sales
{
  // 売り上げクラス
  public class Sale
  {
    // 店舗名
    public string ShopName { get; set; }
    // 商品カテゴリ
    public string ProductCategory { get; set; }
    // 売り上げ高
    public int Amount { get; set; }
  }
}

 

SalesCounter.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Sales
{
  // 売り上げ集計クラス
  public class SalesCounter
  {
    private List<Sale> _sales;

    // コンストラクタ
    public SalesCounter( string filePath )
    {
      _sales = ReadSales( filePath );
    }

    // 店舗別売り上げを求めるメソッド
    public Dictionary<string, int> GetPerStoreSales()
    {
      Dictionary<string, int> dict = new Dictionary<string, int>();
      foreach ( Sale sale in _sales )
      {
        if ( dict.ContainsKey( sale.ShopName ) )
        {
          dict[sale.ShopName] += sale.Amount;
        }
        else
        {
          dict[sale.ShopName] = sale.Amount;
        }
      }
      return dict;
    }

    // 売り上げデータを読み込み、Sale オブジェクトのリストを返す
    static List<Sale> ReadSales( string filePath )
    {
      List<Sale> sales = new List<Sale>();
      string[] lines = File.ReadAllLines( filePath ); // ファイルの内容を一度に読み出す

      foreach ( string line in lines ) // 1行ずつ解析する
      {
        string[] items = line.Split( ',' );
        Sale sale = new Sale  // Sale オブジェクトを生成する
        {
          ShopName = items[0],
          ProductCategory = items[1],
          Amount = int.Parse( items[2] )
        };
        sales.Add( sale );    // Sale オブジェクトを List に追加する
      }

      return sales;
    }
  }
}

 

Program.cs
  • SalesCounter オブジェクトを作成するのみに変更した
  • ReadSales メソッドは SalesCounter に移管した
using System;
using System.Collections.Generic; // List を使うために必要
using System.IO;

namespace Sales
{
  class Program
  {
    static void Main( string[] args )
    {
      // sales.csv は「C:\Users\neko\cs\Sales\Sales\bin\Debug\netcoreapp2.1\.」に置いた
      SalesCounter sales = new SalesCounter( "sales.csv" );
      Dictionary<string, int> amountPerStore = sales.GetPerStoreSales();

      foreach ( KeyValuePair<string, int> obj in amountPerStore )
      {
        Console.WriteLine( "{0} {1}", obj.Key, obj.Value );
      }
    }
  }
}

 

2.2.8 クラスをインターフェイスに置き換える

  • インタフェースの学習を目的とする
  • 本プログラムに対してインタフェースを導入する利点は以下である
    • SalesCounter のコンストラクタで List ではなく、配列を受け取るように変更する場合でも実装変更が不要である
    • SalesCounter クラスの中で同オブジェクトが書き換えられてしまう心配が無くなる。
      • IEnumerable には remove, add というメソッドが無いためである
SalesCounter.cs
  • 次のように変更している。
    //private List<Sale> _sales;
    private IEnumerable<Sale> _sales;
...
    // public Dictionary<string, int> GetPerStoreSales()
    public IDictionary<string, int> GetPerStoreSales()
...
    // static List<Sale> ReadSales( string filePath )
    static IEnumerable<Sale> ReadSales( string filePath )
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Sales
{
  // 売り上げ集計クラス
  public class SalesCounter
  {
    //private List<Sale> _sales;
    private IEnumerable<Sale> _sales;

    // コンストラクタ
    public SalesCounter( string filePath )
    {
      _sales = ReadSales( filePath );
    }

    // 店舗別売り上げを求めるメソッド
    // public Dictionary<string, int> GetPerStoreSales()
    public IDictionary<string, int> GetPerStoreSales()
    {
      Dictionary<string, int> dict = new Dictionary<string, int>();
      foreach ( Sale sale in _sales )
      {
        if ( dict.ContainsKey( sale.ShopName ) )
        {
          dict[sale.ShopName] += sale.Amount;
        }
        else
        {
          dict[sale.ShopName] = sale.Amount;
        }
      }
      return dict;
    }

    // 売り上げデータを読み込み、Sale オブジェクトのリストを返す
    // static List<Sale> ReadSales( string filePath )
    static IEnumerable<Sale> ReadSales( string filePath )
    {
      List<Sale> sales = new List<Sale>();
      string[] lines = File.ReadAllLines( filePath ); // ファイルの内容を一度に読み出す

      foreach ( string line in lines ) // 1行ずつ解析する
      {
        string[] items = line.Split( ',' );
        Sale sale = new Sale  // Sale オブジェクトを生成する
        {
          ShopName = items[0],
          ProductCategory = items[1],
          Amount = int.Parse( items[2] )
        };
        sales.Add( sale );    // Sale オブジェクトを List に追加する
      }

      return sales;
    }
  }
}

 

Program.cs
  • 次のように変更している
      // Dictionary<string, int> amountPerStore = sales.GetPerStoreSales();
      IDictionary<string, int> amountPerStore = sales.GetPerStoreSales();
using System;
using System.Collections.Generic; // List を使うために必要
using System.IO;

namespace Sales
{
  class Program
  {
    static void Main( string[] args )
    {
      // sales.csv は「C:\Users\neko\cs\Sales\Sales\bin\Debug\netcoreapp2.1\.」に置いた
      SalesCounter sales = new SalesCounter( "sales.csv" );
      // Dictionary<string, int> amountPerStore = sales.GetPerStoreSales();
      IDictionary<string, int> amountPerStore = sales.GetPerStoreSales();

      foreach ( KeyValuePair<string, int> obj in amountPerStore )
      {
        Console.WriteLine( "{0} {1}", obj.Key, obj.Value );
      }
    }
  }
}

 

Sale.cs

変更点なし。

using System;
using System.Collections.Generic;
using System.Text;

namespace Sales
{
  // 売り上げクラス
  public class Sale
  {
    // 店舗名
    public string ShopName { get; set; }
    // 商品カテゴリ
    public string ProductCategory { get; set; }
    // 売り上げ高
    public int Amount { get; set; }
  }
}

 

2.2.9 var による暗黙の型指定をする

2.2.10 完成したコード

次のように右辺で型が明確になっている場合は、左辺を var 型として簡潔に済ませてしまって良い。

    public IDictionary<string, int> GetPerStoreSales()
    {
      // Dictionary<string, int> dict = new Dictionary<string, int>();
      var dict = new Dictionary<string, int>();
    static IEnumerable<Sale> ReadSales( string filePath )
    {
      // List<Sale> sales = new List<Sale>();
      var sales = new List<Sale>();
      // SalesCounter sales = new SalesCounter( "sales.csv" );
      var sales =  new SalesCounter( "sales.csv" );
      // IDictionary<string, int> amountPerStore = sales.GetPerStoreSales();
      var amountPerStore = sales.GetPerStoreSales();

 
完成したコードは以下である。

Sale.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace Sales
{
  // 売り上げクラス
  public class Sale
  {
    // 店舗名
    public string ShopName { get; set; }
    // 商品カテゴリ
    public string ProductCategory { get; set; }
    // 売り上げ高
    public int Amount { get; set; }
  }
}

 

SalesCounter.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Sales
{
  // 売り上げ集計クラス
  public class SalesCounter
  {
    private IEnumerable<Sale> _sales;

    // コンストラクタ
    public SalesCounter( string filePath )
    {
      _sales = ReadSales( filePath );
    }

    // 店舗別売り上げを求めるメソッド
    public IDictionary<string, int> GetPerStoreSales()
    {
      var dict = new Dictionary<string, int>();
      foreach ( Sale sale in _sales )
      {
        if ( dict.ContainsKey( sale.ShopName ) )
        {
          dict[sale.ShopName] += sale.Amount;
        }
        else
        {
          dict[sale.ShopName] = sale.Amount;
        }
      }
      return dict;
    }

    // 売り上げデータを読み込み、Sale オブジェクトのリストを返す
    static IEnumerable<Sale> ReadSales( string filePath )
    {
      var sales = new List<Sale>();
      string[] lines = File.ReadAllLines( filePath ); // ファイルの内容を一度に読み出す

      foreach ( string line in lines ) // 1行ずつ解析する
      {
        string[] items = line.Split( ',' );
        Sale sale = new Sale  // Sale オブジェクトを生成する
        {
          ShopName = items[0],
          ProductCategory = items[1],
          Amount = int.Parse( items[2] )
        };
        sales.Add( sale );    // Sale オブジェクトを List に追加する
      }

      return sales;
    }
  }
}

 

Program.cs
using System;
using System.Collections.Generic; // List を使うために必要
using System.IO;

namespace Sales
{
  class Program
  {
    static void Main( string[] args )
    {
      // sales.csv は「C:\Users\neko\cs\Sales\Sales\bin\Debug\netcoreapp2.1\.」に置いた
      var sales =  new SalesCounter( "sales.csv" );

      var amountPerStore = sales.GetPerStoreSales();

      foreach ( KeyValuePair<string, int> obj in amountPerStore )
      {
        Console.WriteLine( "{0} {1}", obj.Key, obj.Value );
      }
    }
  }
}