C#: 総合プロジェクト:タスクマネージャー
1. 要件分析
以下の機能を備えたコマンドラインタスクマネージャーを構築します:
- タスクの追加、削除、完了
- 優先度でフィルタリングおよび集計する
- データをJSONファイルに保存する
- メインの処理をブロックせずに非同期で保存する
- 包括的な例外処理
この機能は7つのステップに分けられ、段階的に実装されています。
2. クラスの設計:列挙型とデータモデル
まず、優先度の列挙型とタスクエンティティクラスを定義します。
▶ サンプル
CSHARP
enum Priority
{
Low,
Medium,
High
}
class Task
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public Priority Priority { get; set; }
public bool IsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
public Task(int id, string title, string description, Priority priority)
{
Id = id;
Title = title;
Description = description;
Priority = priority;
IsCompleted = false;
CreatedAt = DateTime.Now;
}
public override string ToString()
{
string status = IsCompleted ? "[x]" : "[ ]";
return $"{Id,-3} {status} {Priority,-6} {CreatedAt:yyyy-MM-dd} {Title}";
}
}
3. コレクションの保存:TaskManager と CRUD 操作
List<Task>を使用してタスクを保存し、基本的な作成、読み取り、更新、削除の操作を実行します。
▶ サンプル
CSHARP
class TaskManager
{
private List<Task> tasks = new List<Task>();
private int nextId = 1;
public Task AddTask(string title, string description, Priority priority)
{
Task task = new Task(nextId++, title, description, priority);
tasks.Add(task);
return task;
}
public bool RemoveTask(int id)
{
Task task = tasks.Find(t => t.Id == id);
if (task != null)
{
tasks.Remove(task);
return true;
}
return false;
}
public bool CompleteTask(int id)
{
Task task = tasks.Find(t => t.Id == id);
if (task != null)
{
task.IsCompleted = true;
return true;
}
return false;
}
public List<Task> GetAllTasks()
{
return new List<Task>(tasks);
}
public void SortByPriority()
{
tasks.Sort((a, b) => b.Priority.CompareTo(a.Priority));
}
}
4. ファイルへの永続化:JSONのシリアライズとデシリアライズ
System.Text.Jsonを使用すると、タスクリストをファイルに保存したり、ファイルから読み込んだりできます。
▶ サンプル
CSHARP
📖 参照専用
using System.Text.Json;
class TaskManager
{
private List<Task> tasks = new List<Task>();
private int nextId = 1;
private const string FilePath = "tasks.json";
private static readonly JsonSerializerOptions jsonOptions = new JsonSerializerOptions
{
WriteIndented = true
};
public string SaveToFile()
{
try
{
string json = JsonSerializer.Serialize(tasks, jsonOptions);
File.WriteAllText(FilePath, json);
return "Save succeeded.";
}
catch (Exception ex)
{
return $"Save failed: {ex.Message}";
}
}
public string LoadFromFile()
{
try
{
if (!File.Exists(FilePath))
{
return "File does not exist, skipping load.";
}
string json = File.ReadAllText(FilePath);
List<Task>? loaded = JsonSerializer.Deserialize<List<Task>>(json);
if (loaded != null)
{
tasks = loaded;
nextId = tasks.Count > 0 ? tasks.Max(t => t.Id) + 1 : 1;
}
return "Load succeeded.";
}
catch (JsonException ex)
{
return $"JSON format error: {ex.Message}";
}
catch (Exception ex)
{
return $"Load failed: {ex.Message}";
}
}
}
5. LINQ クエリ:フィルタリングと集計
LINQ を使用して、優先度によるフィルタリング、保留中のタスクの照会、および統計情報の生成を実装します。
▶ サンプル
CSHARP
class TaskManager
{
private List<Task> tasks = new List<Task>();
public List<Task> FilterByPriority(Priority priority)
{
return tasks.Where(t => t.Priority == priority).ToList();
}
public List<Task> GetPendingTasks()
{
return tasks.Where(t => !t.IsCompleted)
.OrderByDescending(t => t.Priority)
.ToList();
}
public Dictionary<Priority, int> GetStatistics()
{
return tasks.GroupBy(t => t.Priority)
.ToDictionary(g => g.Key, g => g.Count());
}
public int GetCompletedCount()
{
return tasks.Count(t => t.IsCompleted);
}
public int GetPendingCount()
{
return tasks.Count(t => !t.IsCompleted);
}
}
TEXT
📖 参照専用
Sample statistics output:
High: 2, Medium: 3, Low: 1
Completed: 1, Pending: 5
6. 非同期でのファイル保存
async/awaitを使用して、ノンブロッキングのファイル書き込み操作を実装します。
▶ サンプル
CSHARP
📖 参照専用
class TaskManager
{
private List<Task> tasks = new List<Task>();
private const string FilePath = "tasks.json";
private static readonly JsonSerializerOptions jsonOptions = new JsonSerializerOptions
{
WriteIndented = true
};
public async Task````<string>```` SaveToFileAsync()
{
try
{
string json = JsonSerializer.Serialize(tasks, jsonOptions);
await File.WriteAllTextAsync(FilePath, json);
return "Async save succeeded.";
}
catch (UnauthorizedAccessException ex)
{
return $"File access denied: {ex.Message}";
}
catch (IOException ex)
{
return $"IO error: {ex.Message}";
}
catch (Exception ex)
{
return $"Async save failed: {ex.Message}";
}
}
public async Task````<string>```` LoadFromFileAsync()
{
try
{
if (!File.Exists(FilePath))
{
return "File does not exist, skipping load.";
}
string json = await File.ReadAllTextAsync(FilePath);
List<Task>? loaded = JsonSerializer.Deserialize<List<Task>>(json);
if (loaded != null)
{
tasks = loaded;
nextId = tasks.Count > 0 ? tasks.Max(t => t.Id) + 1 : 1;
}
return "Async load succeeded.";
}
catch (JsonException ex)
{
return $"JSON format error: {ex.Message}";
}
catch (Exception ex)
{
return $"Async load failed: {ex.Message}";
}
}
}
7. コマンドライン対話型インターフェース
switchステートメントを使用して、コマンドラインメニューのループを構築します。
▶ サンプル
CSHARP
📖 参照専用
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
TaskManager manager = new TaskManager();
string loadResult = await manager.LoadFromFileAsync();
Console.WriteLine(loadResult);
while (true)
{
Console.WriteLine("\n===== Task Manager =====");
Console.WriteLine("1. Add task");
Console.WriteLine("2. Delete task");
Console.WriteLine("3. Complete task");
Console.WriteLine("4. List all tasks");
Console.WriteLine("5. Filter by priority");
Console.WriteLine("6. View statistics");
Console.WriteLine("7. Save to file");
Console.WriteLine("0. Exit");
Console.Write("Select: ");
string? input = Console.ReadLine();
switch (input)
{
case "1":
Console.Write("Title: ");
string? title = Console.ReadLine() ?? "";
Console.Write("Description: ");
string? desc = Console.ReadLine() ?? "";
Console.Write("Priority (Low/Medium/High): ");
string? priStr = Console.ReadLine();
if (Enum.TryParse<Priority>(priStr, true, out Priority pri))
{
Task added = manager.AddTask(title, desc, pri);
Console.WriteLine($"Added: {added}");
}
else
{
Console.WriteLine("Invalid priority.");
}
break;
case "2":
Console.Write("Task ID: ");
if (int.TryParse(Console.ReadLine(), out int delId))
{
Console.WriteLine(manager.RemoveTask(delId) ? "Deleted." : "Not found.");
}
break;
case "3":
Console.Write("Task ID: ");
if (int.TryParse(Console.ReadLine(), out int compId))
{
Console.WriteLine(manager.CompleteTask(compId) ? "Completed." : "Not found.");
}
break;
case "4":
manager.SortByPriority();
foreach (Task t in manager.GetAllTasks())
{
Console.WriteLine(t);
}
break;
case "5":
Console.Write("Priority (Low/Medium/High): ");
if (Enum.TryParse<Priority>(Console.ReadLine(), true, out Priority filterPri))
{
foreach (Task t in manager.FilterByPriority(filterPri))
{
Console.WriteLine(t);
}
}
break;
case "6":
Dictionary<Priority, int> stats = manager.GetStatistics();
foreach (var kv in stats)
{
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
Console.WriteLine($"Completed: {manager.GetCompletedCount()}, Pending: {manager.GetPendingCount()}");
break;
case "7":
string saveResult = await manager.SaveToFileAsync();
Console.WriteLine(saveResult);
break;
case "0":
string finalSave = await manager.SaveToFileAsync();
Console.WriteLine(finalSave);
Console.WriteLine("Goodbye!");
return;
default:
Console.WriteLine("Invalid selection.");
break;
}
}
}
}
TEXT
📖 参照専用
===== Task Manager =====
1. Add task
2. Delete task
3. Complete task
4. List all tasks
5. Filter by priority
6. View statistics
7. Save to file
0. Exit
Select: 1
Title: Learn C#
Description: Complete lesson 36
Priority (Low/Medium/High): High
Added: 1 [ ] High 2026-06-28 Learn C#
8. プロジェクト体制の構築
名前空間とusingを使用してコードを整理し、異なる役割を別々のファイルに配置します。
▶ サンプル
プロジェクトのファイル構成:
TEXT
📖 参照専用
TaskManagerApp/
├── Models/
│ ├── Priority.cs
│ └── Task.cs
├── Services/
│ └── TaskManager.cs
└── Program.cs
Priority.cs:
CSHARP
namespace TaskManagerApp.Models
{
public enum Priority
{
Low,
Medium,
High
}
}
Task.cs:
CSHARP
using System;
namespace TaskManagerApp.Models
{
public class Task
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public Priority Priority { get; set; }
public bool IsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
public Task(int id, string title, string description, Priority priority)
{
Id = id;
Title = title;
Description = description;
Priority = priority;
IsCompleted = false;
CreatedAt = DateTime.Now;
}
public override string ToString()
{
string status = IsCompleted ? "[x]" : "[ ]";
return $"{Id,-3} {status} {Priority,-6} {CreatedAt:yyyy-MM-dd} {Title}";
}
}
}
TaskManager.cs:
CSHARP
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using TaskManagerApp.Models;
namespace TaskManagerApp.Services
{
public class TaskManager
{
private List<Models.Task> tasks = new List<Models.Task>();
private int nextId = 1;
private const string FilePath = "tasks.json";
private static readonly JsonSerializerOptions jsonOptions = new JsonSerializerOptions
{
WriteIndented = true
};
public Models.Task AddTask(string title, string description, Priority priority)
{
Models.Task task = new Models.Task(nextId++, title, description, priority);
tasks.Add(task);
return task;
}
public bool RemoveTask(int id)
{
Models.Task? task = tasks.Find(t => t.Id == id);
if (task != null)
{
tasks.Remove(task);
return true;
}
return false;
}
public bool CompleteTask(int id)
{
Models.Task? task = tasks.Find(t => t.Id == id);
if (task != null)
{
task.IsCompleted = true;
return true;
}
return false;
}
public List<Models.Task> GetAllTasks()
{
return new List<Models.Task>(tasks);
}
public void SortByPriority()
{
tasks.Sort((a, b) => b.Priority.CompareTo(a.Priority));
}
public List<Models.Task> FilterByPriority(Priority priority)
{
return tasks.Where(t => t.Priority == priority).ToList();
}
public List<Models.Task> GetPendingTasks()
{
return tasks.Where(t => !t.IsCompleted)
.OrderByDescending(t => t.Priority)
.ToList();
}
public Dictionary<Priority, int> GetStatistics()
{
return tasks.GroupBy(t => t.Priority)
.ToDictionary(g => g.Key, g => g.Count());
}
public int GetCompletedCount()
{
return tasks.Count(t => t.IsCompleted);
}
public int GetPendingCount()
{
return tasks.Count(t => !t.IsCompleted);
}
public async Task````<string>```` SaveToFileAsync()
{
try
{
string json = JsonSerializer.Serialize(tasks, jsonOptions);
await File.WriteAllTextAsync(FilePath, json);
return "Save succeeded.";
}
catch (UnauthorizedAccessException ex)
{
return $"Access denied: {ex.Message}";
}
catch (IOException ex)
{
return $"IO error: {ex.Message}";
}
catch (Exception ex)
{
return $"Save failed: {ex.Message}";
}
}
public async Task````<string>```` LoadFromFileAsync()
{
try
{
if (!File.Exists(FilePath))
{
return "File does not exist, skipping load.";
}
string json = await File.ReadAllTextAsync(FilePath);
List<Models.Task>? loaded = JsonSerializer.Deserialize<List<Models.Task>>(json);
if (loaded != null)
{
tasks = loaded;
nextId = tasks.Count > 0 ? tasks.Max(t => t.Id) + 1 : 1;
}
return "Load succeeded.";
}
catch (JsonException ex)
{
return $"JSON format error: {ex.Message}";
}
catch (Exception ex)
{
return $"Load failed: {ex.Message}";
}
}
}
}
Program.cs:
CSHARP
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using TaskManagerApp.Models;
using TaskManagerApp.Services;
namespace TaskManagerApp
{
class Program
{
static async Task Main()
{
TaskManager manager = new TaskManager();
string loadResult = await manager.LoadFromFileAsync();
Console.WriteLine(loadResult);
while (true)
{
Console.WriteLine("\n===== Task Manager =====");
Console.WriteLine("1. Add task");
Console.WriteLine("2. Delete task");
Console.WriteLine("3. Complete task");
Console.WriteLine("4. List all tasks");
Console.WriteLine("5. Filter by priority");
Console.WriteLine("6. View statistics");
Console.WriteLine("7. Save to file");
Console.WriteLine("0. Exit");
Console.Write("Select: ");
string? input = Console.ReadLine();
switch (input)
{
case "1":
Console.Write("Title: ");
string? title = Console.ReadLine() ?? "";
Console.Write("Description: ");
string? desc = Console.ReadLine() ?? "";
Console.Write("Priority (Low/Medium/High): ");
string? priStr = Console.ReadLine();
if (Enum.TryParse<Priority>(priStr, true, out Priority pri))
{
Models.Task added = manager.AddTask(title, desc, pri);
Console.WriteLine($"Added: {added}");
}
else
{
Console.WriteLine("Invalid priority.");
}
break;
case "2":
Console.Write("Task ID: ");
if (int.TryParse(Console.ReadLine(), out int delId))
{
Console.WriteLine(manager.RemoveTask(delId) ? "Deleted." : "Not found.");
}
break;
case "3":
Console.Write("Task ID: ");
if (int.TryParse(Console.ReadLine(), out int compId))
{
Console.WriteLine(manager.CompleteTask(compId) ? "Completed." : "Not found.");
}
break;
case "4":
manager.SortByPriority();
foreach (Models.Task t in manager.GetAllTasks())
{
Console.WriteLine(t);
}
break;
case "5":
Console.Write("Priority (Low/Medium/High): ");
if (Enum.TryParse<Priority>(Console.ReadLine(), true, out Priority filterPri))
{
foreach (Models.Task t in manager.FilterByPriority(filterPri))
{
Console.WriteLine(t);
}
}
break;
case "6":
Dictionary<Priority, int> stats = manager.GetStatistics();
foreach (var kv in stats)
{
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
Console.WriteLine($"Completed: {manager.GetCompletedCount()}, Pending: {manager.GetPendingCount()}");
break;
case "7":
string saveResult = await manager.SaveToFileAsync();
Console.WriteLine(saveResult);
break;
case "0":
string finalSave = await manager.SaveToFileAsync();
Console.WriteLine(finalSave);
Console.WriteLine("Goodbye!");
return;
default:
Console.WriteLine("Invalid selection.");
break;
}
}
}
}
}
9. 完全なシングルファイル版
迅速なテストを行うために、すべてのコードを1つのコンパイル可能なファイルに統合してください。
▶ サンプル
CSHARP
📖 参照専用
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
enum Priority
{
Low,
Medium,
High
}
class Task
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public Priority Priority { get; set; }
public bool IsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
public Task(int id, string title, string description, Priority priority)
{
Id = id;
Title = title;
Description = description;
Priority = priority;
IsCompleted = false;
CreatedAt = DateTime.Now;
}
public override string ToString()
{
string status = IsCompleted ? "[x]" : "[ ]";
return $"{Id,-3} {status} {Priority,-6} {CreatedAt:yyyy-MM-dd} {Title}";
}
}
class TaskManager
{
private List<Task> tasks = new List<Task>();
private int nextId = 1;
private const string FilePath = "tasks.json";
private static readonly JsonSerializerOptions jsonOptions = new JsonSerializerOptions
{
WriteIndented = true
};
public Task AddTask(string title, string description, Priority priority)
{
Task task = new Task(nextId++, title, description, priority);
tasks.Add(task);
return task;
}
public bool RemoveTask(int id)
{
Task? task = tasks.Find(t => t.Id == id);
if (task != null)
{
tasks.Remove(task);
return true;
}
return false;
}
public bool CompleteTask(int id)
{
Task? task = tasks.Find(t => t.Id == id);
if (task != null)
{
task.IsCompleted = true;
return true;
}
return false;
}
public List<Task> GetAllTasks()
{
return new List<Task>(tasks);
}
public void SortByPriority()
{
tasks.Sort((a, b) => b.Priority.CompareTo(a.Priority));
}
public List<Task> FilterByPriority(Priority priority)
{
return tasks.Where(t => t.Priority == priority).ToList();
}
public List<Task> GetPendingTasks()
{
return tasks.Where(t => !t.IsCompleted)
.OrderByDescending(t => t.Priority)
.ToList();
}
public Dictionary<Priority, int> GetStatistics()
{
return tasks.GroupBy(t => t.Priority)
.ToDictionary(g => g.Key, g => g.Count());
}
public int GetCompletedCount()
{
return tasks.Count(t => t.IsCompleted);
}
public int GetPendingCount()
{
return tasks.Count(t => !t.IsCompleted);
}
public async Task````<string>```` SaveToFileAsync()
{
try
{
string json = JsonSerializer.Serialize(tasks, jsonOptions);
await File.WriteAllTextAsync(FilePath, json);
return "Save succeeded.";
}
catch (UnauthorizedAccessException ex)
{
return $"Access denied: {ex.Message}";
}
catch (IOException ex)
{
return $"IO error: {ex.Message}";
}
catch (Exception ex)
{
return $"Save failed: {ex.Message}";
}
}
public async Task````<string>```` LoadFromFileAsync()
{
try
{
if (!File.Exists(FilePath))
{
return "File does not exist, skipping load.";
}
string json = await File.ReadAllTextAsync(FilePath);
List<Task>? loaded = JsonSerializer.Deserialize<List<Task>>(json);
if (loaded != null)
{
tasks = loaded;
nextId = tasks.Count > 0 ? tasks.Max(t => t.Id) + 1 : 1;
}
return "Load succeeded.";
}
catch (JsonException ex)
{
return $"JSON format error: {ex.Message}";
}
catch (Exception ex)
{
return $"Load failed: {ex.Message}";
}
}
}
class Program
{
static async Task Main()
{
TaskManager manager = new TaskManager();
string loadResult = await manager.LoadFromFileAsync();
Console.WriteLine(loadResult);
while (true)
{
Console.WriteLine("\n===== Task Manager =====");
Console.WriteLine("1. Add task");
Console.WriteLine("2. Delete task");
Console.WriteLine("3. Complete task");
Console.WriteLine("4. List all tasks");
Console.WriteLine("5. Filter by priority");
Console.WriteLine("6. View statistics");
Console.WriteLine("7. Save to file");
Console.WriteLine("0. Exit");
Console.Write("Select: ");
string? input = Console.ReadLine();
switch (input)
{
case "1":
Console.Write("Title: ");
string? title = Console.ReadLine() ?? "";
Console.Write("Description: ");
string? desc = Console.ReadLine() ?? "";
Console.Write("Priority (Low/Medium/High): ");
string? priStr = Console.ReadLine();
if (Enum.TryParse<Priority>(priStr, true, out Priority pri))
{
Task added = manager.AddTask(title, desc, pri);
Console.WriteLine($"Added: {added}");
}
else
{
Console.WriteLine("Invalid priority.");
}
break;
case "2":
Console.Write("Task ID: ");
if (int.TryParse(Console.ReadLine(), out int delId))
{
Console.WriteLine(manager.RemoveTask(delId) ? "Deleted." : "Not found.");
}
break;
case "3":
Console.Write("Task ID: ");
if (int.TryParse(Console.ReadLine(), out int compId))
{
Console.WriteLine(manager.CompleteTask(compId) ? "Completed." : "Not found.");
}
break;
case "4":
manager.SortByPriority();
foreach (Task t in manager.GetAllTasks())
{
Console.WriteLine(t);
}
break;
case "5":
Console.Write("Priority (Low/Medium/High): ");
if (Enum.TryParse<Priority>(Console.ReadLine(), true, out Priority filterPri))
{
foreach (Task t in manager.FilterByPriority(filterPri))
{
Console.WriteLine(t);
}
}
break;
case "6":
Dictionary<Priority, int> stats = manager.GetStatistics();
foreach (var kv in stats)
{
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
Console.WriteLine($"Completed: {manager.GetCompletedCount()}, Pending: {manager.GetPendingCount()}");
break;
case "7":
string saveResult = await manager.SaveToFileAsync();
Console.WriteLine(saveResult);
break;
case "0":
string finalSave = await manager.SaveToFileAsync();
Console.WriteLine(finalSave);
Console.WriteLine("Goodbye!");
return;
default:
Console.WriteLine("Invalid selection.");
break;
}
}
}
}
TEXT
📖 参照専用
Load succeeded.
===== Task Manager =====
1. Add task
2. Delete task
3. Complete task
4. List all tasks
5. Filter by priority
6. View statistics
7. Save to file
0. Exit
Select: 1
Title: Learn C#
Description: Complete lesson 36
Priority (Low/Medium/High): High
Added: 1 [ ] High 2026-06-28 Learn C#
Select: 1
Title: Write documentation
Description: Organize project docs
Priority (Low/Medium/High): Medium
Added: 2 [ ] Medium 2026-06-28 Write documentation
Select: 3
Task ID: 2
Completed.
Select: 4
2 [x] Medium 2026-06-28 Write documentation
1 [ ] High 2026-06-28 Learn C#
Select: 6
High: 1
Medium: 1
Completed: 1, Pending: 1
Select: 0
Save succeeded.
Goodbye!
❓ よくある質問
Q シリアライズにはなぜパラメータなしのコンストラクタが必要なのですか?
A System.Text.Json ではデフォルトでパラメータなしのコンストラクタが必須となっています。JsonSerializerOptions を使用してコンバータをカスタマイズすることで、この要件を回避することができます。
Q List.Find で要素が見つからなかった場合はどうなりますか?
A Find は例外をスローすることなく null を返します。結果を使用する前に、必ず null チェックを行ってください。
Q 非同期メソッドに
await が含まれていない場合はどうなりますか?A そのタスクは「ファイア・アンド・フォーゲット」状態となり、例外が捕捉されず、データ損失につながる可能性があります。
Q Enum.TryParse は失敗した場合、何を返しますか?
A false を返し、出力パラメータはデフォルト値のままとなり、例外はスローされません。
📖 まとめ
- 列挙型は有限の状態の集合を定義し、コードの可読性を高める
- エンティティクラスはデータと動作をカプセル化しており、ToString メソッドはフォーマットされた出力を提供します
- リスト
<T>は、Find/Sort/Where と組み合わせてコレクション操作を実行します - System.Text.Json は、軽量なシリアライズおよびデシリアライズ機能を提供します
- LINQのWhere、OrderBy、GroupByを使用することで、宣言型クエリが可能になります
- async/await はノンブロッキングなファイル I/O を実装しています
- try-catch はまず特定の例外を捕捉し、その後、Exception にフォールバックする
- switch文を使ってコマンドラインメニューのループを構築する
- 名前空間と、複数ファイルからなるプロジェクト構造の整理
📝 練習問題
- Task クラスに DueDate プロパティを追加し、「期限切れのタスクを表示する」機能を実装する
- TaskManager に、タイトルと説明の変更をサポートする EditTask メソッドを追加する
- 保存形式をJSONからCSVに変更し、独自のシリアライズロジックを実装する
- 作成日による並べ替えとタイトルによる検索機能を追加する
- コマンドライン引数モードを実装する:プログラムは起動時に引数に基づいて操作を実行し、その後終了する