C# - 无法弄清楚如何将自定义对象读取到文件、添加新的自定义对象以及将新集写入覆盖的文件

C# - Can't figure out how to read custom objects to file, add new custom objects, and write new set to overwritten file

我知道这可能令人困惑,所以让我解释一下。我正在尝试制作一个模仿日历程序的程序,它几乎可以工作。我只是难以写入输出文件。我有一个可以写入文件的自定义对象 Event,我可以从文件中读取字符串并根据该信息创建新对象。但是,我在尝试向旧的 Events 添加新的 Events 然后将所有这些信息写回文件时遇到了困难,同时还覆盖了文件的最新版本。我在我的程序中加入了评论,希望这能让它更清晰一些。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Globalization;
using System.IO;

// Date: 4/24/17

// Purpose (according to Reddit spec)
/* create a program that will allow you to enter events organizable by hour. There must be menu options of some form, and you must be 
 * able to easily edit, add, and delete events without directly changing the source code.
 * (note that by menu i dont necessarily mean gui. as long as you can easily access the different options and receive prompts and 
 * instructions telling you how to use the program, it will probably be fine) */

namespace ChallengeOneIntermediate
{
   class Event
   {
      string eventName;
      DateTime eventDay;
      DateTime eventTime;

      public string EventName
      {
         get { return eventName; }
         set { eventName = value; }
      }

      public DateTime EventDay
      {
         get { return eventDay; }
         set { eventDay = value; }
      }

      public DateTime EventTime
      {
         get { return eventTime; }
         set { eventTime = value; }
      }

      public Event(string name, DateTime day, DateTime time)
      {
         eventName = name;
         eventDay = day;
         eventTime = time;
      }
   }

   class Program
   {
      public static List<string> ADD_CHOICES = new List<string> { "1", "1.", "add", "add event", "1 add event", "1. add event", 
                                                                 "1.add event", "1addevent", "1add event", "1.add event",
                                                                 "1 addevent", "1. addevent", "1add", "1.add", "1 add", "1. add" };

      public static List<string> DEL_CHOICES = new List<string> { "2", "2.", "delete", "delete event", "2 delete event", 
                                                                 "2. delete event", "2deleteevent", "2.deleteevent", "2delete event", 
                                                                 "2.delete event", "2 deleteevent", "2. deleteevent", "2delete",
                                                                 "2.delete", "2 delete", "2. delete" };

      public static List<string> EDIT_CHOICES = new List<string> { "3", "3.", "edit", "edit event", "3 edit event", "3. edit event",
                                                                  "3editevent", "3.editevent", "3edit event", "3.edit event",
                                                                  "3 editevent", "3. editevent", "3edit", "3.edit", "3 edit",
                                                                  "3. edit" };

      public static List<string> VIEW_CHOICES = new List<string> { "4", "4.", "view", "calendar", "viewcalendar", "view calendar",
                                                                  "4.view", "4. view", "4.calendar", "4. calendar",
                                                                  "4.viewcalendar", "4.view calendar", "4. viewcalendar",
                                                                  "4. view calendar", "4viewcalendar", "4view calendar",
                                                                  "4 viewcalendar", "4 view calendar" };

      public static List<string> EXIT_CHOICES = new List<string> { "5", "5.", "exit", "5 exit", "5. exit", "5exit", "5.exit" };

      public static List<Event> EVENT_CALENDAR = new List<Event>();

      public static string PATH = @"G:\Daily Programmer\C#\Intermediate\Challenge #1\calendar.txt";

      static void Main(string[] args)
      {
         int menuChoice;

         while (true)
         {
            menuChoice = Menu();

            if (menuChoice == 1)
               AddEvent();
            else if (menuChoice == 2)
               DeleteEvent();
            else if (menuChoice == 3)
               EditEvent();
            else if (menuChoice == 4)
               ViewCalendar();
            else if (menuChoice == 5)
               break;
            else
               Console.WriteLine("Sorry, that's not a valid choice.\n");
         }

         // No point in writing to the file if there's nothing in EVENT_CALENDAR to write
         if (EVENT_CALENDAR.Count > 0)
            WriteToFile();

         Console.ReadLine();
      }

      static void AddEvent()
      {
         DateTime newDate, newTime;
         string eventName, eventDate, eventTime;

         Console.Write("Enter the name of your event, or 'exit' to go back to the menu: ");
         eventName = Console.ReadLine();

         if (EXIT_CHOICES.Contains(eventName, StringComparer.OrdinalIgnoreCase))
            return;

         Console.Write("Enter the date of your event in the format MM/DD/YYYY: ");
         eventDate = Console.ReadLine();

         Console.Write("Enter the time of your event in the format HH:MM AM/PM: ");
         eventTime = Console.ReadLine();

         try
         {
            DateTime.TryParse(eventDate, out newDate);
            DateTime.TryParse(eventTime, out newTime);
            Event newCalendarEvent = new Event(eventName, newDate, newTime);
            EVENT_CALENDAR.Add(newCalendarEvent);

            // Uses LINQ to sort by day, then by time
            EVENT_CALENDAR = EVENT_CALENDAR.OrderBy(x => x.EventDay).ThenBy(x => x.EventTime).ToList();

            Console.WriteLine("Event: " + eventName + " added successfully!");
         }
         catch (FormatException fe)
         {
            Console.WriteLine(fe.Message);
            Console.WriteLine("Sorry, you didn't enter the date and time in the right format. Please try again.");
         }
      }

      static void DeleteEvent()
      {
         string userInput;
         int num = 1, eventToDelete;

         // Easiest way I could think of in a non-visual format to identify which event the user is trying to delete
         foreach (Event eachEvent in EVENT_CALENDAR)
         {
            Console.WriteLine(num + ". " + eachEvent.EventName + ", {0:MM/dd/yyyy} @ {1:hh:mm tt}", 
                              eachEvent.EventDay, eachEvent.EventTime);
            num++;
         }

         Console.WriteLine();

         Console.Write("Enter the number of the event you want to delete, or 'exit' to go back to the menu: ");
         userInput = Console.ReadLine();

         if (EXIT_CHOICES.Contains(userInput, StringComparer.OrdinalIgnoreCase))
            return;
         else
         {
            Int32.TryParse(userInput, out eventToDelete);

            if (eventToDelete != 0 && (eventToDelete < 1 || eventToDelete > EVENT_CALENDAR.Count))
               while (eventToDelete < 1 || eventToDelete > EVENT_CALENDAR.Count)
               {
                  Console.WriteLine("Sorry, invalid choice. Please enter another number: ");
                  eventToDelete = Convert.ToInt32(Console.ReadLine());
               }
         }

         EVENT_CALENDAR.RemoveAt(eventToDelete - 1);

         // No need to re-sort EVENT_CALENDAR because order is maintained

         Console.WriteLine("Event deleted successfully!");
      }

      static void EditEvent()
      {
         DateTime newDate, newTime;
         string eventName, eventDate, eventTime, userInput;
         int num = 1, eventToEdit;

         // Including the sort here too because for some reason it wasn't appropriately sorting events added with blank names
         // Uses LINQ to sort by day, then by time
         EVENT_CALENDAR = EVENT_CALENDAR.OrderBy(x => x.EventDay).ThenBy(x => x.EventTime).ToList();

         foreach (Event eachEvent in EVENT_CALENDAR)
         {
            Console.WriteLine(num + ". " + eachEvent.EventName + ", {0:MM/dd/yyyy} @ {1:hh:mm tt}",
                              eachEvent.EventDay, eachEvent.EventTime);
            num++;
         }

         Console.WriteLine();

         Console.Write("Enter the number of the event you'd like to edit, or 'exit' to return to the menu: ");
         userInput = Console.ReadLine();

         if (EXIT_CHOICES.Contains(userInput, StringComparer.OrdinalIgnoreCase))
            return;
         else
         {
            Int32.TryParse(userInput, out eventToEdit);

            if (eventToEdit != 0 && (eventToEdit < 1 || eventToEdit > EVENT_CALENDAR.Count))
               while (eventToEdit < 1 || eventToEdit > EVENT_CALENDAR.Count)
               {
                  Console.WriteLine("Sorry, invalid choice. Please enter another number: ");
                  eventToEdit = Convert.ToInt32(Console.ReadLine());
               }
         }

         Console.Write("What is the new name of the event? ");
         eventName = Console.ReadLine();

         Console.Write("Enter the new date of your event in the format MM/DD/YYYY: ");
         eventDate = Console.ReadLine();

         Console.Write("Enter the new time of your event in the format HH:MM AM/PM: ");
         eventTime = Console.ReadLine();

         try
         {
            // eventToEdit acts as index + 1 here, so I have to subtract 1 to get the right index internally
            EVENT_CALENDAR[eventToEdit - 1].EventName = eventName;

            DateTime.TryParse(eventDate, out newDate);
            EVENT_CALENDAR[eventToEdit - 1].EventDay = newDate;

            DateTime.TryParse(eventTime, out newTime);
            EVENT_CALENDAR[eventToEdit - 1].EventTime = newTime;

            // Uses LINQ to order by day, then by time
            EVENT_CALENDAR = EVENT_CALENDAR.OrderBy(x => x.EventDay).ThenBy(x => x.EventTime).ToList();

            Console.WriteLine("Event: " + eventName + " edited successfully!");
         }
         catch (FormatException fe)
         {
            Console.WriteLine(fe.Message);
            Console.WriteLine("Sorry, you didn't enter the date and time in the right format. Please try again.");
         }
      }

      static int IntMenuChoice(string stringMenuChoice)
      {
         if (ADD_CHOICES.Contains(stringMenuChoice, StringComparer.OrdinalIgnoreCase))
            return 1;
         else if (DEL_CHOICES.Contains(stringMenuChoice, StringComparer.OrdinalIgnoreCase))
            return 2;
         else if (EDIT_CHOICES.Contains(stringMenuChoice, StringComparer.OrdinalIgnoreCase))
            return 3;
         else if (VIEW_CHOICES.Contains(stringMenuChoice, StringComparer.OrdinalIgnoreCase))
            return 4;
         else if (EXIT_CHOICES.Contains(stringMenuChoice, StringComparer.OrdinalIgnoreCase))
            return 5;
         else
            return -1;
      }

      static int Menu()
      {
         string strChoice;
         int intChoice;

         Console.WriteLine();

         Console.WriteLine("-- Options --\n");
         Console.WriteLine("1. Add event\n");
         Console.WriteLine("2. Delete event\n");
         Console.WriteLine("3. Edit event\n");
         Console.WriteLine("4. View calendar\n");
         Console.WriteLine("5. Exit\n");
         Console.Write("Choice: ");
         strChoice = Console.ReadLine();
         Console.WriteLine();

         intChoice = IntMenuChoice(strChoice);

         return intChoice;
      }

      // 
      /* The purpose of this function is to try and merge old sessions of a user's calendar with a new session.
       * It's unlikely the user has this program running all the time, and what's the point of having a calendar that only stores
       * events on a session-to-session basis?
       * What I'm attempting to do here is read events from the existing file, create a new list of Event objects based on
       * the fields stored in each line by splitting on spaces, and then assign that sorted list to EVENT_CALENDAR.
       * It seems to work locally (I tested by running a foreach() loop on EVENT_CALENDAR), but not in WriteToFile(). */
      static void SortExistingFile()
      {
         List<Event> existingDates = new List<Event>();
         DateTime existDate, existTime;

         foreach(string fileLine in File.ReadLines(PATH))
         {
            // Split() has to split on characters, not strings, so single quotes
            string[] fields = fileLine.Split(' ');
            DateTime.TryParse(fields[1], out existDate);
            DateTime.TryParse(fields[2], out existTime);
            existingDates.Add(new Event(fields[0], existDate, existTime));
         }

         // Orders all the events read from the file
         existingDates = existingDates.OrderBy(x => x.EventDay).ThenBy(x => x.EventTime).ToList();

         /* I decided to clear the old calendar and indivudally re-write events to it because I wasn't sure
          * if trying to do assignment (ex. EVENT_CALENDAR = existingDates) would only work locally and not
          * maintain its state after the function exits */
         EVENT_CALENDAR.Clear();

         foreach(Event oldEvent in existingDates)
            EVENT_CALENDAR.Add(oldEvent);
      }

      static void ViewCalendar()
      {
         int num = 1;

         if (EVENT_CALENDAR.Count == 0)
         {
            Console.WriteLine("There's nothing in the calendar!");
            return;
         }

         foreach (Event eachEvent in EVENT_CALENDAR)
         {
            Console.WriteLine(num + ". " + eachEvent.EventName + ", {0:MM/dd/yyyy} @ {1:hh:mm tt}",
                              eachEvent.EventDay, eachEvent.EventTime);
            num++;
         }
      }

      static void WriteToFile()
      {
         // I know that this probably hurts performance because then the file's being written to 1 + 2 + 3 + ... + n times
         // I wasn't sure of another way I could write Event objects to a file in human-readable format
         try
         {
            foreach(Event eachEvent in EVENT_CALENDAR)
            {
               if (File.Exists(PATH))
               {
                  // I append all the new events to the existing events in the file
                  File.AppendAllText(PATH, String.Format("{0} {1:MM/dd/yyyy} {2:hh:mm tt}" + Environment.NewLine, eachEvent.EventName,
                                       eachEvent.EventDay, eachEvent.EventTime));
               }
               else
               {
                  FileStream myFile = File.Create(PATH);
                  myFile.Close();
                  File.WriteAllText(PATH, String.Format("{0} {1:MM/dd/yyyy} {2:hh:mm tt}" + Environment.NewLine, eachEvent.EventName,
                                    eachEvent.EventDay, eachEvent.EventTime));
               }
            }
         }
         catch (ArgumentNullException ane)
         {
            Console.WriteLine("ArgumentNullException\n");
            Console.WriteLine(ane.Message);
         }
         catch (IOException ioe)
         {
            Console.WriteLine("IOException\n");
            Console.WriteLine(ioe.Message);
         }
         catch (ArgumentException ae)
         {
            Console.WriteLine("ArgumentException\n");
            Console.WriteLine(ae.Message);
         }
         catch (Exception e)
         {
            Console.WriteLine("Exception\n");
            Console.WriteLine(e.Message);
         }

         // Here's where I sort all the existing events that merged with the new events and store them in EVENT_CALENDAR
         SortExistingFile();
         string allEvents = String.Empty;

         // This is what I can't get to work
         /* My intent here was to store everything in one massive string.
          * Why do this? I want to overwrite the file each time the program closes so all events are in order.
          * I couldn't get File.Copy(PATH, PATH, true) to work.
          * WriteAllText() does what I want, but it wouldn't work in a foreach() loop because it would always just overwrite
          * the previous event, so only the most recent event in the calendar would be stored. */
         foreach(Event eachEvent in EVENT_CALENDAR)
         {
            allEvents += eachEvent.EventName + " " + String.Format("{0:MM/dd/yyyy}", eachEvent.EventDay.ToString()) +
                         " " + String.Format("{0:hh:mm tt", eachEvent.EventTime.ToString()) + Environment.NewLine;
         }

         // If the above code worked, then I could do this and all the events would be stored in the calendar file.
         File.WriteAllText(PATH, allEvents);
      }
   }
}

Clarity Edit: 我无法让副本工作,因为我在尝试写入已经打开的文件时抛出错误(我相信这是因为我试图从 PATH 复制到 PATH)。我想到了用换行符将所有内容写入一个字符串,然后将其存储在一个文件中的想法。但是,我收到一个错误,指出 DateTime 对象无效,尽管它在用户输入它时有效并且我将其转换为 DateTime 对象。 WriteAllText 工作正常。我只是无法在 foreach 循环中使用它,因为在循环的每次迭代中调用 WriteAllText 会覆盖当前会话中写入的最后一个事件。

我知道这不是专门作为控制台应用程序制作的理想程序,但它几乎完全按预期工作,除了从文件读取并覆盖回同一文件的这一方面。这是来自 DailyProgrammer 的挑战,我决定通过添加将日历写入文件的功能来稍微增强它。

NullReferenceException 问题,但为什么呢? 感谢 JohnG,我能够重写 SortExistingFile() 函数以正确读取,并重写我的 WriteToFile() 函数以正确覆盖。但是,现在我在 SortExistingFile() 中的 eventFieldArray 上收到 NullReferenceException,即使我已经尝试使用不同的值对其进行初始化。我不知道是什么原因造成的。这是修改后的功能:

static void SortExistingFile()
      {
         List<Event> existingDates = new List<Event>();
         DateTime existDateTime;
         string readLine;
         string[] eventFieldArray = null;
         long fileLength = new FileInfo(PATH).Length;

         // Check if file has any contents so there's no attempt to read the file when it doesn't contain anything
         if (fileLength == 0)
            return;
         else
         {
            try
            {
               using (StreamReader sr = new StreamReader(PATH))
               {
                  while (fileLength > 0)
                  {
                     readLine = sr.ReadLine();
                     eventFieldArray = readLine.Split(',');
                     DateTime.TryParse(eventFieldArray[1], out existDateTime);
                     existingDates.Add(new Event(eventFieldArray[0], existDateTime));

                     fileLength--;
                  }
               }
            }
            catch (Exception e)
            {
               Console.WriteLine("Error: " + e.Message);
               Console.ReadLine();
            }
         }

         EVENT_CALENDAR = existingDates.OrderBy(x => x.EventDateTime).ToList();
      }

我红色了一点代码,我觉得这个问题有点混乱。我在您的代码中找不到从文本文件加载某种数据模型的位置。因此,我首先想到的是你的做法是错误的。

首先,你应该知道这样的程序应该由数据库支持(甚至 SQLite),这将使你能够有效地读取事件,文本文件不是理想的方法这种情况,尤其是if/when会有很多事件。

如果您想继续使用文本文件,我的建议如下:

  1. 为您的日历事件(例如:CalEvent)创建一个新的 class,然后可以在 List<CalEvent> 中使用它来保存您的整个日历。
  2. List<CalEvent> 序列化为 Json 文本文件,使用类似 Json.NET 的方式将其存储在驱动器上。
  3. 反序列化 Json 文本文件并取回 List<CalEvent>

然后,例如,您可以使用 LINQ 在 List<CalEvent> 中搜索事件,如下所示:

List<CalEvent> calEvents = readJsonFileOutput();
DateTime now = DateTime.Now;
DateTime twodays = DateTime.Now.AddDays(2);
IEnumerable<CalEvent> eventsInTheNextTwoDays = calEvents.Where(e => e.DateAndTime >= now && e.DateAndTime <= twodays).ToList();

如果您绝对需要在文本文件中以排序的方式存储这些事件,您可以使用 calEvents.OrderBy(e => e.DateAndTime)。显然所有这些代码只是一个例子。

我看到的问题是您从未读入您保存的数据。我知道您想通过将这些数据写入文件来保存这些数据……但除非您将其读回,否则它是无用的。

在你的例子中,当程序启动时......它应该去寻找文件calander.txt,如果找到,读入现有事件以允许用户view/edit/delete这些已经存在的事件。当您读入这些事件时,您可以将它们按原样放入一个列表中,并且您可以像往常一样从该列表中添加、编辑和删除事件。当用户添加、删除或更新事件时,您在列表中更新此事件,然后您只需将整个列表写入文件并覆盖以前的版本。如果您从未读回数据,那么一开始就没有理由保存它。

如果您将整个 "calendar.txt" 文件读入一个事件对象列表,那么对列表所做的添加、更改或删除将包含更新的数据,所以...鉴于此,每次添加一个事件、删除或更改,您只需将更新事件对象列表写入文件。不需要检查它是否存在,如果不存在则创建一个新文件,如果存在则直接覆盖它。然后当你稍后再次 运行 程序时,它会读入之前的更改。这似乎是你想要做的。

下面是一个简单的写入方法,它采用事件对象列表和字符串文件名将列表写入文件。写入此文件后,您应该更改代码以在程序启动时将此文件读取到事件对象列表中。我希望这是有道理的。

下面的代码使用了 StreamWriter,应该也能正常工作。我所做的另一个更改是 Event class 有两个 DataTime 对象……一个用于日期,另一个用于时间……这是不必要的,因为一个 DateTime 对象可以并且应该包含事件的日期和时间。最后的变化是代码似乎使用空格“”作为文件中事件名称和事件日期时间的分隔符。我将其更改为有一个逗号“,”分隔符,这样您就可以使用带空格的事件名称。希望这有帮助。

private void WriteToFile(List<Event> allEvents, string filepath) {
  try {
    using (StreamWriter sw = new StreamWriter(filepath)) {
      foreach (Event curEvent in allEvents) {
         sw.WriteLine(curEvent.EventName + "," + curEvent.EventDateTime.ToString());
      }
    }
  }
  catch (Exception e) {
    Console.WriteLine("Write Error: " + e.Message);
    Console.ReadKey();
  }
}

编辑:使用 IComparable 接口排序...

查看您的排序方法……可能有一种更简单的方法来对列表进行排序,方法是为 Events class 实现 IComparable<Event> 接口,如下所示。 CompareTo 将根据事件名称对两个 Event 对象进行排序,如果事件名称相同,则为事件日期。

要实现这个...将接口放在 Event 签名中,如下所示:

public class Event : IComparable<Event> {

之后,你添加IComparable<Event>接口...编译器会报错CompareTo下面的方法没有实现。添加 CompareTo 方法来解决这个问题。

public int CompareTo(Event other) {
  int result = this.eventName.CompareTo(other.eventName);
  if (result == 0) {
    return this.eventDateTime.CompareTo(other.eventDateTime);
  } else {
    return result;
  }
}

之后,您可以对事件对象列表进行排序,如下所示:

EVENT_CALENDAR.Sort();

上面的调用将使用 Event class 中的 CompareTo 方法对列表进行排序。恕我直言,比 SortExistingFile() 方法容易得多。