当没有流打开时,在路径上共享违规

本文关键字:路径 共享 | 更新日期: 2023-09-27 18:11:01

我正在用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方法不使用流,所以我不知道如何保持打开状态。事实上,在关卡的这一点上,阅读流根本没有打开。

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

当没有流打开时,在路径上共享违规

没有足够的信息可以确定,但是如果你看一下这段代码:

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块而不是单独的方法调用来打开/关闭文件。

您说异常在第28行和第29行抛出。

第29行

本行为File.WriteAllText()呼叫。

对于第29行,我可以很容易地看到发生了什么。File.Create()返回的FileStream呼叫.Dispose()失败。File.Create(path)new FileStream(path, FileMode.Create)的简写。由于未指定,因此使用默认值FileShare.None。这意味着对File.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()在该方法退出之前不会运行。如果在这个时间间隔内确实发生了垃圾收集,那么从File.Create()返回的FileStream的终结器可能在File.WriteAllText()打开文件之前运行。如果发生这种情况,那么您将不会在第29行看到异常。

28行

本行为File.Create()呼叫。

对于第28行发生的异常,我只能猜测发生了什么。从您的代码中不清楚将调用类的公共方法的顺序。然而,我想到了一种可能性。

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

  1. 第28行第一次在特定区域运行并返回FileStream。我们把它命名为FileStream a。
  2. 第29行抛出一个异常,因为 FileStreama持有一个打开的文件句柄。
  3. 第29行的异常被外部代码捕获。
  4. 外部代码再次调用SaveAll()
  5. 第28行试图再次为同一区域打开第二个FileStream,并抛出一个异常,因为 FileStreama仍然持有一个打开的文件句柄。
  6. 最终垃圾收集器运行并调用 FileStreama的终结器。在此之后,该区域的代码将通过第28行(但可能在第29行失败)。

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

建议

我不相信这与错误有关,但我必须提一下。我强烈建议不要像使用saveFileReader那样使用字段变量来保存TextReader。这将作用域与变量的生命周期分离开来。相反,你应该把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());
}
看到了

?不需要字段

一般来说,如果可能的话,您应该避免使用字段或属性,如果它们引用的对象具有基于作用域的生存期,因为在使用字段或属性时更容易出错。如果可以使用局部变量和using,则更容易编写正确的代码。此外,这种技术增加了可读性,因为您可以知道变量只能从方法内部访问,而不可能被同一类中的任何其他方法访问。