0

我正在用 C# 开发一个游戏,其中每个“地区”都会定期将有关它的数据存储在保存文件中。为了测试,“SaveAll”方法在关卡开始时被调用一次。

文件操作的代码如下:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.IO;

public class DistrictSaveData : KeepAwake {

    private string saveDirectoryPath = string.Concat(
        System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments),
        "\\My Games\\District\\Districts");
    private string saveFilePath = string.Concat(
        System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments),
        "\\My Games\\District\\Districts\\District-x.dat");
    private StreamReader saveFileReader;

    public void SaveAll() {
        foreach (GameObject gO in GameObject.FindGameObjectsWithTag("District")) {
            District district = gO.GetComponent<District>();
            saveFilePath = string.Concat(
                System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments),
                "\\My Games\\District\\Districts\\District-", district.id , ".dat");
            if (!Directory.Exists(saveDirectoryPath)) {
                Directory.CreateDirectory(saveDirectoryPath);
            }
            try {
            File.Delete(saveFilePath);
            } catch {}
            File.Create(saveFilePath);
            File.WriteAllText(saveFilePath, district.SendSaveData());
        }
    }

    public void LoadAll() {
        foreach (GameObject gO in GameObject.FindGameObjectsWithTag("District")) {
            District district = gO.GetComponent<District>();
            saveFilePath = string.Concat(
                System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments),
                "\\My Games\\District\\Districts\\District-", district.id , ".dat");
            if(File.Exists(saveFilePath)) {
                OpenFileForReading();
                district.isHQ = bool.Parse(saveFileReader.ReadLine());
                district.controllingFaction = StringToFaction(saveFileReader.ReadLine());
                district.agricultureSpecialisation = StringToAgricultureSpecialisation(saveFileReader.ReadLine());
                district.technologySpecialisation = StringToTechnologySpecialisation(saveFileReader.ReadLine());
                district.militarySpecialisation = StringToMilitarySpecialisation(saveFileReader.ReadLine());
                CloseFileAfterReading();
            } else
                break;
        }
    }

    /// <summary>
    /// Opens the save file for reading.
    /// </summary>
    private void OpenFileForReading() {
        saveFileReader = File.OpenText(saveFilePath);
    }

    /// <summary>
    /// Closes the save file after reading.
    /// </summary>
    private void CloseFileAfterReading() {
        saveFileReader.Close();
    }

    private Faction StringToFaction(string stringToConvert) {
        switch (stringToConvert) {
        case "TheCrimsonLegion":
            return Faction.TheCrimsonLegion;
        case "TheVanguardsOfChaos":
            return Faction.TheVanguardsOfChaos;
        case "TheEmeraldFoxes":
            return Faction.TheEmeraldFoxes;
        case "TheSyndicate":
            return Faction.TheSyndicate;
        case "TheKeepersOfTheTome":
            return Faction.TheKeepersOfTheTome;
        case "TheArchitectsOfThought":
            return Faction.TheArchitectsOfThought;
        default:
            return Faction.None;
        }
    }

    private AgricultureSpecialisation StringToAgricultureSpecialisation(string stringToConvert) {
        switch (stringToConvert) {
        case "Farm":
            return AgricultureSpecialisation.Farm;
        case "Plantation":
            return AgricultureSpecialisation.Plantation;
        case "Biodome":
            return AgricultureSpecialisation.Biodome;
        default:
            return AgricultureSpecialisation.None;
        }
    }

    private TechnologySpecialisation StringToTechnologySpecialisation(string stringToConvert) {
        switch (stringToConvert) {
        case "Laboratory":
            return TechnologySpecialisation.Laboratory;
        case "University":
            return TechnologySpecialisation.University;
        case "GreatTechnologicalInstitution":
            return TechnologySpecialisation.GreatTechnologicalInstitution;
        default:
            return TechnologySpecialisation.None;
        }
    }

    private MilitarySpecialisation StringToMilitarySpecialisation(string stringToConvert) {
        switch (stringToConvert) {
        case "Outpost":
            return MilitarySpecialisation.Outpost;
        case "Barracks":
            return MilitarySpecialisation.Barracks;
        case "Fortress":
            return MilitarySpecialisation.Fortress;
        default:
            return MilitarySpecialisation.None;
        }
    }
}

抛出的异常(多次)读取:

IOException: Sharing violation on path C:\users\samdy1\My Documents\My Games\District\Districts\District-0.dat
System.IO.FileStream..ctor (System.String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, Boolean anonymous, FileOptions options) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.IO/FileStream.cs:320)
System.IO.FileStream..ctor (System.String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize)
(wrapper remoting-invoke-with-check) System.IO.FileStream:.ctor (string,System.IO.FileMode,System.IO.FileAccess,System.IO.FileShare,int)
System.IO.File.Create (System.String path, Int32 bufferSize) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.IO/File.cs:135)
System.IO.File.Create (System.String path) (at /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.IO/File.cs:130)
DistrictSaveData+<SaveAll>c__Iterator3.MoveNext () (at Assets/Scripts/_Core/SaveData/DistrictSaveData.cs:29)

此异常在 SaveAll 方法中进程的第 28 行和第 29 行引发。但是,SaveAll 方法不使用流,所以我看不到如何保持打开状态。事实上,在关卡的这个点上,阅读流根本没有打开。

我错过了什么明显的东西吗?

4

2 回答 2

2

没有足够的信息可以确定,但是如果您查看此代码:

OpenFileForReading();
district.isHQ = bool.Parse(saveFileReader.ReadLine());
district.controllingFaction = StringToFaction(saveFileReader.ReadLine());
district.agricultureSpecialisation = StringToAgricultureSpecialisation(saveFileReader.ReadLine());
district.technologySpecialisation = StringToTechnologySpecialisation(saveFileReader.ReadLine());
district.militarySpecialisation = StringToMilitarySpecialisation(saveFileReader.ReadLine());
CloseFileAfterReading();

如果在 OpenFileForReading() 之后但在 CloseFileAfterReading() 之前引发异常,则不会关闭文件,您将看到您描述的行为。

至少,将代码重写为

OpenFileForReading();
try
{
    district.isHQ = bool.Parse(saveFileReader.ReadLine());
    district.controllingFaction = StringToFaction(saveFileReader.ReadLine());
    district.agricultureSpecialisation = StringToAgricultureSpecialisation(saveFileReader.ReadLine());
    district.technologySpecialisation = StringToTechnologySpecialisation(saveFileReader.ReadLine());
    district.militarySpecialisation = StringToMilitarySpecialisation(saveFileReader.ReadLine());
}
finally
{
    CloseFileAfterReading();
}

using尽管使用块而不是单独的方法调用来打开/关闭文件,但您会更好。

于 2015-09-03T22:00:31.377 回答
1

你说异常是在第 28 行和第 29 行抛出的。

29号线

这条线是指File.WriteAllText()呼叫。

对于第 29 行,我可以很容易地看到发生了什么。您未能调用返回.Dispose()的. 是 的简写。由于未指定,因此使用默认值。这意味着后续调用将失败。FileStreamFile.Create()File.Create(path)new FileStream(path, FileMode.Create)FileShare.NoneFile.WriteAllText()

而不是这种模式,你应该删除File.Create(). File.WriteAllText()将已经创建或截断文件,因此无需预先创建或手动截断它。如果你坚持调用File.Create()(这是无意义的),你应该using像下面这样调用它,以确保它的句柄在调用之前关闭File.WriteAllText()

using (File.Create(saveFilePath)) {
}
File.WriteAllText(saveFilePath, district.SendSaveData());

File.Create()在返回之后和File.WriteAllText()尝试打开文件之前发生垃圾收集是可能的,但不太可能发生。例如,如果district.SendSaveData()创建大量对象并使用大量内存,则可能会发生这种情况。这种行为可能(但不保证)导致垃圾收集发生。此外,方法的所有参数始终在实际方法调用之前进行评估,因此File.WriteAllText()在该方法退出之前不会运行。如果垃圾收集确实在此时间间隔内发生,则FileStream返回的终结器可能会在打开文件File.Create()之前运行。File.WriteAllText()如果发生这种情况,那么您将不会在第 29 行看到异常。

28号线

这条线是指File.Create()呼叫。

对于第 28 行发生的异常,我只能猜测发生了什么。从您的代码中不清楚将调用您的类的公共方法的顺序。但是,有一种可能性浮现在脑海中。

如果您对SaveAll()from 代码的调用从第 29 行捕获异常,然后SaveAll()在没有垃圾收集和终结器运行的情况下再次调用,则第 28 行应该抛出共享冲突异常。这是事件的顺序:

  1. 第 28 行第一次运行特定区域并返回一个FileStream. 让我们将这个人命名为FileStreamA。
  2. FileStream由于A 持有相关文件的打开文件句柄,第29 行引发异常。
  3. 第 29 行的异常被外部代码捕获。
  4. 外部代码SaveAll()再次调用。
  5. 第 28 行尝试再次为同一区域打开第二个,并由于A 仍持有相关文件的打开文件句柄FileStream而引发异常。FileStream
  6. 最终,垃圾收集器运行并调用FileStreamA 的终结器。发生这种情况后,该区域的代码将通过第 28 行(但很可能在第 29 行失败)。

另一种可能性是文件正在任何其他进程中打开或由程序的另一部分打开。

建议

我不认为这与错误有关,但我必须提到它。我强烈建议不要TextReader像使用saveFileReader. 这将范围与变量的生命周期分离。相反,您应该将TextReader作为参数传递给方法,使用 控制其生命周期using,并避免将其保存到不受范围规则控制的变量(例如字段),除非您正在创建一个本身支持范围规则的便利对象(例如,通过实施IDisposable)。

关于您的代码,我注意到的一件事是您甚至从未利用字段这一事实saveFileReader。您使用该变量,就像它是本地变量一样。它是一个字段而不是本地的这一事实向代码的任何读者表明,您可以从多个方法中引用它,或者打算在其中存储一个必须在多个调用中保留的值。例如,StringToAgricultureSpecialisation()如果您打算首先将其设置为字段,那么访问该变量是有意义的。但是,您只能使用OpenFileForReading()和将其作为字段访问CloseFileForReading()

我将仅演示如何在您的代码中使用范围模式:

using (var saveFileReader = File.OpenText(saveFilePath)) {
    district.isHQ = bool.Parse(saveFileReader.ReadLine());
    district.controllingFaction = StringToFaction(saveFileReader.ReadLine());
    district.agricultureSpecialisation = StringToAgricultureSpecialisation(saveFileReader.ReadLine());
    district.technologySpecialisation = StringToTechnologySpecialisation(saveFileReader.ReadLine());
    district.militarySpecialisation = StringToMilitarySpecialisation(saveFileReader.ReadLine());
}

看?不需要领域。

通常,如果可能,如果字段或属性引用具有基于范围的生命周期的对象,则应避免使用它们,因为使用字段或属性时更容易出错。如果您可以改用局部变量 and using,则更容易编写正确的代码。此外,这种技术提高了可读性,因为您可以知道该变量只能从方法内访问,而不是可能被同一类中的任何其他方法访问。

于 2018-05-25T16:09:04.380 回答