課題の進め方

基本的には適当なプロジェクト/ソリューションを作って課題を進めた上で,Program.csのみ(特に他に指示がなければ)を提出する.

他の人(受講者・非受講者両方)に解答内容(一部でも)を見せない,そして他の人の解答内容(一部でも)を見ないようお願いします.特に,公開の場所に解答を置かないようお願いします.githubやbitbucket等は使える人は使えばよいと思いますが,privateレポジトリにするようにお願いします.

プロジェクト/ソリューションの作成

Note

再掲:プロジェクトは一つの実行形式やライブラリを作成するためのコード等を全てまとめたものであり,ソリューションは関連するプロジェクトをまとめたもの(参考:What are solutions and projects in Visual Studio).

適当な名前(たとえば課題3の解答なのでQ3にするなど)のフォルダを適当な場所に作成し, VSCodeで作成したフォルダを開く.そして,VSCode内のターミナルで以下を実行する.

dotnet new console -o .

課題の実施

Program.cs を問題文の指示の通りに編集する(提出・採点手続きの簡略化のため提出する.csファイルは一つのみ).作成した.csファイルには先頭部分に学籍番号と名前をコメントとして含めること.なので,たとえば学籍番号Z0TB9999の東北 大学さんの提出ファイルは

// Z0TB9999
// 東北 大学

という行から始まる.

提出

できあがった Program.csをClassroom内の当該回の「課題」より提出する.最初のステップで作成したフォルダにあるはず.また問題文に指示がある場合はそのファイル(例:課題4のitems.txt)も提出する.提出前には以下を確認しよう.

基本課題

以下のItemクラスを考える.

// 以下の内容は変更しない.
class Item : System.IEquatable<Item>
{
   // 商品名
   public string Name { get; }
   // 値段
   public int Price { get; }

   public Item(string s, int p)
   {
      Name = s;
      Price = p;
   }

   // デバッグ用.課題で直接は使用しない.
   public override string ToString()
   {
      // {Name = Chocolate, Price = 250} みたいな文字列に
      return "{Name = " + Name + ", Price = " + Price + "}";
   }

   // 以下はItemオブジェクト同士の等価性検査を行えるようにするためのもの.
   // 本課題を進める上で理解する必要はない.
   // 
   // 参考: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type
   // ここに書かれている通り,C#的にはclassでなくrecordを使うべきではある.

   // object.Equals のオーバライド
   // ジェネリックでないコレクションはこっちを使う場合がある?
   // ?が付いているのはnull許容(参照)型
   public override bool Equals(object? o)
   {
      // e as T は (T) e に近いが, (T) eと異なり T へのキャストが失敗したらnullとなる.
      // null はどこも指していない参照.
      return Equals(o as Item);
   }

   // インタフェース System.IEquatable<Item> のメソッド Equals の定義
   // List<Item>やLinkedList<Item>のRemove等はこっちを使う
   public bool Equals(Item? o)
   {
      // &&の短絡を利用.oがnull のときは o.Name や o.Price は 評価されない.
      // (object?)がないと,二つ下で定義した演算子が呼ばれ無限ループする
      return (object?) o != null && Name == o.Name && Price == o.Price;
   }

   // aとbがItem型であるときに a == bと書いたときに呼ばれる.
   // aとbがItem型であるときにも a.Equals(b) は a が Itemの派生型のインスタンスであるときには
   // 上のメソッドは呼ばれない場合がある.対し,a == b はこのメソッドが呼ばれることに注意.
   public static bool operator ==(Item a, Item b)
   {
      if ((object) a == null)
      {
            return (object) b == null;
      }
      return a.Equals(b);
   }
   public static bool operator !=(Item a, Item b)
   {
      return !(a == b);
   }

   // 今回は関係ないが GetHashCode も実装しないと 
   // HashSet や Dictionary につっこんだときの挙動が変になる.
   public override int GetHashCode()
   {
      return (Name, Price).GetHashCode();
   }
}

そのときItemで表される商品を扱うためのショッピングカートを表すクラス Cart を作成せよ.

カートは以下の2種類の情報を管理する.

また,以下のpublicなコンストラクタ,メソッド,およびプロパティを持つ.

メンバ 説明
Cart(int budget) 予算がbudgetであるような空のショッピングカートを作成するコンストラクタ.
void Add(Item i) 商品iのカートへの追加を試みるメソッド.ただし,iを追加することでカート内の商品の金額の合計が予算を超えるのならば追加しない.
void Remove(Item i) 商品iをカートから一つ取り除くメソッド.カートに商品iが入っていなければ何も起こらない.
void Report() カート内の全商品,合計金額,購入した場合の予算の残額を標準出力に出力するメソッド.
int Budget 予算を確認するためのread-onlyなプロパティ.予算はカート作成後は変化しない.
int Remaining 予算の残額を確認するためのread-onlyなプロパティ.カートの内容によって値が変化.

ただし,Report()int Remainingの実装にあたっては,for文,while文(資料では紹介していないがdo文も)を使わないこととする.

Note

残額はAdd(Item i)Remove(Item i)が呼び出される都度計算してもよいし,そうしなくてもよい.前者のほうが効率的だが,カートの中身と残額が対応しないというバグの可能性を生むことになる.

提出するプログラムは以下の動作確認用のクラスを含むこと.

class Program
{
   static void Main()
   {
      // 商品
      Item chocolate = new Item("チョコレート", 120);
      Item chip = new Item("チップ", 100);
      Item popCorn = new Item("ポップコーン", 110);
      Item candy = new Item("キャンディ", 20);

      // 予算300のカート
      Cart sc = new Cart(300);

      sc.Add(chocolate);
      sc.Add(chip);
      sc.Add(popCorn); // 予算オーバ追加できず

      sc.Report();

      sc.Remove(chip);
      sc.Add(candy);

      Console.WriteLine();
      sc.Report();

      sc.Add(popCorn);

      Console.WriteLine();
      sc.Report();

      // 名前と値段の同じ商品は等価として扱われる
      sc.Remove(new Item("チョコレート", 120));

      Console.WriteLine();
      sc.Report();

      // カートに入ってないものを除いても何も起こらない
      sc.Remove(chocolate);

      Console.WriteLine();
      sc.Report();

      sc.Add(candy);
      sc.Add(new Item("キャンディ", 40)); // 同じ商品名でも値段が違うことがある
      sc.Add(popCorn);      

      Console.WriteLine();
      sc.Report();

      // 以下をアンコメントするとエラーになる(ようにする)
      // sc.Budget += 1000 

      // 別のカートを作成
      Cart sc2 = new Cart(120); 
      // キャンディを買えるだけカートに入れる
      for (int p = candy.Price; p <= sc2.Budget ; p += candy.Price)
      {
         sc2.Add( candy ); 
      }
      Console.WriteLine(); 
      sc2.Report(); 

      // さらに別のカートを作成
      Cart sc3 = new Cart(128); 
      // キャンディを買えるだけカートに入れる—別のやり方
      while( sc3.Remaining >= candy.Price ) 
      {
         sc3.Add( candy ); 
      }
      Console.WriteLine(); 
      sc3.Report(); 
   }
}

このとき以下の出力が得られるようにする(ただしカートの中身の出力順は異なっていてもよい).

チョコレート: 120
チップ: 100
合計: 220
残額: 80

チョコレート: 120
キャンディ: 20
合計: 140
残額: 160

チョコレート: 120
キャンディ: 20
ポップコーン: 110
合計: 250
残額: 50

キャンディ: 20
ポップコーン: 110
合計: 130
残額: 170

キャンディ: 20
ポップコーン: 110
合計: 130
残額: 170

キャンディ: 20
ポップコーン: 110
キャンディ: 20
キャンディ: 40
ポップコーン: 110
合計: 300
残額: 0

キャンディ: 20
キャンディ: 20
キャンディ: 20
キャンディ: 20
キャンディ: 20
キャンディ: 20
合計: 120
残額: 0

キャンディ: 20
キャンディ: 20
キャンディ: 20
キャンディ: 20
キャンディ: 20
キャンディ: 20
合計: 120
残額: 8

Tip

VS Codeだとデフォルトの設定ではデバッグコンソールにおいて同じ行がまとめて表示される.これをしないようにするには,設定項目のCollapse Identical Linesのチェックをはずす.

発展課題

Important

本課題を完了できたのならば本課題の解答のみを提出すればよく,基本課題の解答は提出する必要はない.

CartクラスのReport()メソッドを変更しカートの中身を表示する際に同じ商品はまとめて表示するようにせよ.

実行例

チョコレート: 120 x 1
チップ: 100 x 1
合計: 220
残額: 80

チョコレート: 120 x 1
キャンディ: 20 x 1
合計: 140
残額: 160

チョコレート: 120 x 1
キャンディ: 20 x 1
ポップコーン: 110 x 1
合計: 250
残額: 50

キャンディ: 20 x 1
ポップコーン: 110 x 1
合計: 130
残額: 170

キャンディ: 20 x 1
ポップコーン: 110 x 1
合計: 130
残額: 170

キャンディ: 20 x 2
ポップコーン: 110 x 2
キャンディ: 40 x 1
合計: 300
残額: 0

キャンディ: 20 x 6
合計: 120
残額: 0

キャンディ: 20 x 6
合計: 120
残額: 8

ヒントList<Item>を使って,既に表示した商品かどうかを管理し,既に表示した商品ならば何もせず,そうでなければカートの中その商品の数を数えるとよいう方法で実装できる.与えられた要素がリストに含まれているかの検査を foreach で書くのは難しくないが,List<T>.Contains(T) メソッドを使うことができる(当該メソッドの詳細).

ただ,第3回では紹介しなかったが連想配列を表すクラスDictionary<Item, int>を使うのがより自然だろう.このクラスがどのようなコンストラクタかメソッドを知るには.NET API リファレンスの当該クラスのページを見るとよい.次回の演習でも少し紹介する.