課題の進め方
基本的には適当なプロジェクト/ソリューションを作って課題を進めた上で,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)も提出する.提出前には以下を確認しよう.
- 提出プログラムコードのファイル名は
Program.cs
になっているか - 提出するファイルに学籍番号と名前がコメントとして含まれているか
- 文献やWebサイトを参考にした場合は文献の情報やURLおよびアクセス日に加えて元文献のどの部分を参考にしたのかがコードの当該箇所付近のコメントとして記述されているか
- 友人と相談した・された場合はその友人の名前および大雑把な相談の内容がコメントとして書かれているか
- ただし,友人の解答を見ない,そして友人に解答を見せないようお願いします
- TA・教員と相談した場合は,その旨と大雑把な相談の内容がコメントとして書かれているか
基本課題
以下のItem
クラスを考える.
// 以下の内容は変更しない.
class Item : System.IEquatable<Item>
{
// 商品名
public string Name { get; }
// 値段
public int Price { get; }
public Item(string s, int p)
{
= s;
Name = p;
Price }
// デバッグ用.課題で直接は使用しない.
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種類の情報を管理する.
- カート内の商品:ユーザは商品(
Item
)を追加したり削除したりできる - 予算:作成時に指定し,予算を超えるときは商品をカートに追加することはできない
また,以下の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()
{
// 商品
= new Item("チョコレート", 120);
Item chocolate = new Item("チップ", 100);
Item chip = new Item("ポップコーン", 110);
Item popCorn = new Item("キャンディ", 20);
Item candy
// 予算300のカート
= new Cart(300);
Cart sc
.Add(chocolate);
sc.Add(chip);
sc.Add(popCorn); // 予算オーバ追加できず
sc
.Report();
sc
.Remove(chip);
sc.Add(candy);
sc
.WriteLine();
Console.Report();
sc
.Add(popCorn);
sc
.WriteLine();
Console.Report();
sc
// 名前と値段の同じ商品は等価として扱われる
.Remove(new Item("チョコレート", 120));
sc
.WriteLine();
Console.Report();
sc
// カートに入ってないものを除いても何も起こらない
.Remove(chocolate);
sc
.WriteLine();
Console.Report();
sc
.Add(candy);
sc.Add(new Item("キャンディ", 40)); // 同じ商品名でも値段が違うことがある
sc.Add(popCorn);
sc
.WriteLine();
Console.Report();
sc
// 以下をアンコメントするとエラーになる(ようにする)
// sc.Budget += 1000
// 別のカートを作成
= new Cart(120);
Cart sc2 // キャンディを買えるだけカートに入れる
for (int p = candy.Price; p <= sc2.Budget ; p += candy.Price)
{
.Add( candy );
sc2}
.WriteLine();
Console.Report();
sc2
// さらに別のカートを作成
= new Cart(128);
Cart sc3 // キャンディを買えるだけカートに入れる—別のやり方
while( sc3.Remaining >= candy.Price )
{
.Add( candy );
sc3}
.WriteLine();
Console.Report();
sc3}
}
このとき以下の出力が得られるようにする(ただしカートの中身の出力順は異なっていてもよい).
チョコレート: 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 リファレンスの当該クラスのページを見るとよい.次回の演習でも少し紹介する.