如何在.NET (C#)中本地存储数据

81

我正在编写一个应用程序,以获取用户数据并在本地存储以供后续使用。该应用程序将经常启动和停止,并且我希望在应用程序启动/结束时保存/加载数据。

如果使用平面文件,它会非常简单,因为数据不需要得到保护(它只会存储在此台计算机上)。以下是我认为可选的选项:

  • 平面文件
  • XML
  • SQL数据库

与XML相比,平面文件需要更多的维护工作(没有像XML那样的内置类),但是我以前从未使用过XML,而且SQL似乎对这个相对简单的任务来说有些过头了。是否还有其他值得探索的方式?如果没有,哪个是最佳解决方案?


编辑:为了问题增加一些数据,基本上我想要存储的仅是像这样的字典:

Dictionary<string, List<Account>> 

其中 Account 是另一种自定义类型。

我是否应该将字典序列化为 xmlroot,然后将 Account 类型序列化为属性?


更新2:

因此可以序列化字典。让它变得复杂的是,这个字典的值本身是一个泛型,它是类型为 Account 的复杂数据结构列表。每个 Account 都相当简单,只是一堆属性而已。

我的理解是,这里的目标是尝试得到以下结果:

<Username1>
    <Account1>
        <Data1>data1</Data1>
        <Data2>data2</Data2>
    </Account1>
</Username1>
<Username2>
    <Account1>
        <Data1>data1</Data1>
        <Data2>data2</Data2>
    </Account1>
    <Account2>
        <Data1>data1</Data1>
        <Data2>data2</Data2>
    </Account2>
 </Username2>

如您所见,层次结构如下:

  • 用户名 (字典中的字符串) >
  • 帐户 (列表中的每个帐户) >
  • 帐户数据 (即类属性)。

Dictionary<Username, List<Account>>获取此布局是棘手的问题,并且这也是本问题的重点。

这里有很多关于序列化的“如何”回答,这是我的错,因为我之前没有表述清楚,但现在我正在寻找一个明确的解决方案。


请提供有关应用程序类型、存储的数据以及预期大小的更多详细信息。 - AK_
3
用于序列化字典的方法: http://stackoverflow.com/questions/1111724 - Cheeso
19个回答

29

我会将文件存储为JSON格式。由于您要存储的是一个仅包含名称/值对列表的字典,因此这正是json设计的用途。
有相当多不错的免费.NET json库-这里是一个,但您可以在第一个链接中找到完整的列表。


JSON 对于本地存储来说有点不寻常,但绝对是一个很好的解决方案。特别是使用像 Newtonsoft 的 Json.NET 这样的库。 - AFract
2
不依赖第三方库,我会使用内置的数据集类型来存储数据,这种方法非常简单,可以轻松写入磁盘(请参见Tom Miller的示例)。 - EKanadily

24

这真的取决于你要存储什么数据。如果你正在讨论结构化数据,那么 XML 或者一个非常轻量级的 SQL RDBMS(如 SQLite 或 SQL Server Compact Edition)都适合你。如果数据超出了微不足道的大小,则 SQL 解决方案尤其具有吸引力。

如果你要存储大型的相对非结构化的数据(例如图像等二进制对象),那么显然数据库或 XML 解决方案都不合适,但根据你的问题,我猜测它更多地是前者而不是后者。


XML配置文件必须有结构吗? - user195488
1
@Roboto:XML 的定义是有结构的。然而,这并不意味着你必须以高度结构化的方式使用它们。 - Adam Robinson

18
以上所有答案都很好,通常都能解决问题。
如果您需要一种简单、免费的方式来扩展到数百万个数据,请尝试从 GitHubNuGet 获取 ESENT Managed Interface 项目。
ESENT 是 Windows 的可嵌入式数据库存储引擎(ISAM)的一部分。它提供了可靠、事务性、并发性、高性能的数据存储,具有行级锁定、预写式日志和快照隔离。这是 ESENT Win32 API 的托管包装器。
它有一个 PersistentDictionary 对象,非常容易使用。将其视为 Dictionary() 对象,但无需额外代码即可自动从磁盘加载和保存。
例如:
/// <summary>
/// Ask the user for their first name and see if we remember 
/// their last name.
/// </summary>
public static void Main()
{
    PersistentDictionary<string, string> dictionary = new PersistentDictionary<string, string>("Names");
    Console.WriteLine("What is your first name?");
    string firstName = Console.ReadLine();
    if (dictionary.ContainsKey(firstName))
    {
        Console.WriteLine("Welcome back {0} {1}", firstName, dictionary[firstName]);
    }
    else
    {
        Console.WriteLine("I don't know you, {0}. What is your last name?", firstName);
        dictionary[firstName] = Console.ReadLine();
    }

回答George的问题:

支持的键类型

只有以下类型作为字典键才被支持:

Boolean Byte Int16 UInt16 Int32 UInt32 Int64 UInt64 Float Double Guid DateTime TimeSpan String

支持的值类型

字典的值可以是任何键类型、可空版本的键类型、Uri、IPAddress或可序列化结构。如果一个结构满足以下所有标准,则该结构被视为可序列化:

• 该结构被标记为可序列化 • 结构的每个成员都是以下之一: 1. 原始数据类型(例如Int32) 2. 字符串、Uri或IPAddress 3. 可序列化结构。

换句话说,可序列化的结构不能包含对类对象的任何引用。这样做是为了保持API的一致性。向PersistentDictionary添加对象会通过序列化创建对象的副本。修改原始对象不会修改副本,这会导致混乱的行为。为避免这些问题,PersistentDictionary只接受值类型作为值。

可以被序列化 [Serializable] struct Good { public DateTime? Received; public string Name; public Decimal Price; public Uri Url; }

不能被序列化 [Serializable] struct Bad { public byte[] Data; // 数组不被支持 public Exception Error; // 引用对象 }


1
这种方法基本上是用持久字典替换内置的通用字典。这是一个相当优雅的解决方案,但它如何处理像 OP 示例中那样的复杂对象呢?它是将所有东西存储在字典内部,还是只存储字典本身? - George
这可能无法达到尝试保存类型为Account的列表的最终目标。键是可以的,但使通用可序列化可能很困难:/。 - George
1
其他实现此功能的人可能会受益于知道您可以使用Nuget获取ManagedEsent。然后,您需要引用Esent.Collections.DLL和Esent.ISAM.DLL。然后添加“using Microsoft.Isam.Esent.Collections.Generic;”以获取PersistentDictionary类型。集合DLL可能需要从http://managedesent.codeplex.com的下载选项中下载。 - Steve Hibbert
"ManagedEsent" 已更名为 "Microsoft.Database.ManagedEsent"。您应该使用 nuget 中的 "Microsoft.Database.Collections.Generic",因为它包含了 ManagedEsent 和 ISAM。 - VoteCoffee

15

通过序列化,XML 很容易使用。 使用隔离存储

另请参阅如何决定存储每个用户状态的位置? 注册表? AppData? 隔离存储?

public class UserDB 
{
    // actual data to be preserved for each user
    public int A; 
    public string Z; 

    // metadata        
    public DateTime LastSaved;
    public int eon;

    private string dbpath; 

    public static UserDB Load(string path)
    {
        UserDB udb;
        try
        {
            System.Xml.Serialization.XmlSerializer s=new System.Xml.Serialization.XmlSerializer(typeof(UserDB));
            using(System.IO.StreamReader reader= System.IO.File.OpenText(path))
            {
                udb= (UserDB) s.Deserialize(reader);
            }
        }
        catch
        {
            udb= new UserDB();
        }
        udb.dbpath= path; 

        return udb;
    }


    public void Save()
    {
        LastSaved= System.DateTime.Now;
        eon++;
        var s= new System.Xml.Serialization.XmlSerializer(typeof(UserDB));
        var ns= new System.Xml.Serialization.XmlSerializerNamespaces();
        ns.Add( "", "");
        System.IO.StreamWriter writer= System.IO.File.CreateText(dbpath);
        s.Serialize(writer, this, ns);
        writer.Close();
    }
}

1
这不是很便携也不整洁。 - user195488
3
看起来像复制粘贴的代码,所以你可以成为第一个发帖者。最好还是坚持你的链接。 - user195488
9
嗯,是的,我从我写的应用程序中直接剪切出来的,可以做到这一点。Roboto,你有什么问题? - Cheeso
它甚至没有使用依赖注入!你没有收到备忘录吗? - Steve Smith
我同意XML建议,但在尝试后,我遇到了Isolated Storage的限制 - 它会根据代码运行的程序集创建不同的文件。最终我只使用AppData\Roaming通过 Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"{AppName}\{FileName}.XML"); - Ory Zaidenvorm
您现在有一个损坏的链接。 - ΩmegaMan

9
我建议使用XML读写器类来处理文件,因为它可以轻松地进行序列化。 C#中的序列化 序列化(在Python中称为pickling)是将对象转换为二进制表示形式的简单方法,然后可以将其写入磁盘或通过网络发送。 它非常有用,例如可方便地将设置保存到文件中。 如果您标记了自己的类以使用[Serializable]属性,则可以序列化该类的所有成员,除了那些标记为[NonSerialized]的成员。
以下是代码示例,演示如何执行此操作:
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;


namespace ConfigTest
{ [ Serializable() ]

    public class ConfigManager
    {
        private string windowTitle = "Corp";
        private string printTitle = "Inventory";

        public string WindowTitle
        {
            get
            {
                return windowTitle;
            }
            set
            {
                windowTitle = value;
            }
        }

        public string PrintTitle
        {
            get
            {
                return printTitle;
            }
            set
            {
                printTitle = value;
            }
        }
    }
}

你可以在 ConfigForm 中调用 ConfigManager 类并对其进行序列化!
public ConfigForm()
{
    InitializeComponent();
    cm = new ConfigManager();
    ser = new XmlSerializer(typeof(ConfigManager));
    LoadConfig();
}

private void LoadConfig()
{     
    try
    {
        if (File.Exists(filepath))
        {
            FileStream fs = new FileStream(filepath, FileMode.Open);
            cm = (ConfigManager)ser.Deserialize(fs);
            fs.Close();
        } 
        else
        {
            MessageBox.Show("Could not find User Configuration File\n\nCreating new file...", "User Config Not Found");
            FileStream fs = new FileStream(filepath, FileMode.CreateNew);
            TextWriter tw = new StreamWriter(fs);
            ser.Serialize(tw, cm);
            tw.Close();
            fs.Close();
        }    
        setupControlsFromConfig();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

在序列化后,你可以使用cm.WindowTitle等参数来调用配置文件的内容。


5
仅澄清一下:Serializable和NonSerialized对XmlSerializer没有任何影响;它们仅用于System.Runtime.Serialization(例如二进制序列化)。 XmlSerializer序列化公共字段和(读写)属性,而不是内部状态:类上不需要任何属性,并且使用XmlIgnore而不是NonSerialized来排除字段或属性。 - itowlson
@itowlson:正确。XML序列化使用反射生成特殊类来执行序列化。 - user195488
如果代码有缩进且大小一致,阅读代码会更加方便。 - Lasse V. Karlsen
@Lasse:不确定你的意思,但如果阅读起来太困难,那么你可以编辑它。 - user195488

9
如果你的集合变得太大,我发现Xml序列化会变得非常慢。另一种序列化字典的选项是使用BinaryReader和BinaryWriter来“自己动手”。以下是一些示例代码,只是为了让你开始。你可以将它们制作成通用扩展方法来处理任何类型的Dictionary,这很有效,但过于冗长不适合在此处发布。
class Account
{
    public string AccountName { get; set; }
    public int AccountNumber { get; set; }

    internal void Serialize(BinaryWriter bw)
    {
        // Add logic to serialize everything you need here
        // Keep in synch with Deserialize
        bw.Write(AccountName);
        bw.Write(AccountNumber);
    }

    internal void Deserialize(BinaryReader br)
    {
        // Add logic to deserialize everythin you need here, 
        // Keep in synch with Serialize
        AccountName = br.ReadString();
        AccountNumber = br.ReadInt32();
    }
}


class Program
{
    static void Serialize(string OutputFile)
    {
        // Write to disk 
        using (Stream stream = File.Open(OutputFile, FileMode.Create))
        {
            BinaryWriter bw = new BinaryWriter(stream);
            // Save number of entries
            bw.Write(accounts.Count);

            foreach (KeyValuePair<string, List<Account>> accountKvp in accounts)
            {
                // Save each key/value pair
                bw.Write(accountKvp.Key);
                bw.Write(accountKvp.Value.Count);
                foreach (Account account in accountKvp.Value)
                {
                    account.Serialize(bw);
                }
            }
        }
    }

    static void Deserialize(string InputFile)
    {
        accounts.Clear();

        // Read from disk
        using (Stream stream = File.Open(InputFile, FileMode.Open))
        {
            BinaryReader br = new BinaryReader(stream);
            int entryCount = br.ReadInt32();
            for (int entries = 0; entries < entryCount; entries++)
            {
                // Read in the key-value pairs
                string key = br.ReadString();
                int accountCount = br.ReadInt32();
                List<Account> accountList = new List<Account>();
                for (int i = 0; i < accountCount; i++)
                {
                    Account account = new Account();
                    account.Deserialize(br);
                    accountList.Add(account);
                }
                accounts.Add(key, accountList);
            }
        }
    }

    static Dictionary<string, List<Account>> accounts = new Dictionary<string, List<Account>>();

    static void Main(string[] args)
    {
        string accountName = "Bob";
        List<Account> newAccounts = new List<Account>();
        newAccounts.Add(AddAccount("A", 1));
        newAccounts.Add(AddAccount("B", 2));
        newAccounts.Add(AddAccount("C", 3));
        accounts.Add(accountName, newAccounts);

        accountName = "Tom";
        newAccounts = new List<Account>();
        newAccounts.Add(AddAccount("A1", 11));
        newAccounts.Add(AddAccount("B1", 22));
        newAccounts.Add(AddAccount("C1", 33));
        accounts.Add(accountName, newAccounts);

        string saveFile = @"C:\accounts.bin";

        Serialize(saveFile);

        // clear it out to prove it works
        accounts.Clear();

        Deserialize(saveFile);
    }

    static Account AddAccount(string AccountName, int AccountNumber)
    {
        Account account = new Account();
        account.AccountName = AccountName;
        account.AccountNumber = AccountNumber;
        return account;
    }
}

谢谢,这看起来是目前最好的解决方案。您所说的“与反序列化/序列化保持同步”是什么意思?是指在文件被修改时进行更新吗?此函数仅在应用程序启动和退出时用于保存字典,因此能否请您澄清一下?否则非常感谢。 - George
经过一段时间的思考,我意识到这意味着序列化和反序列化的逻辑应该是相同的。就是这样。 - George
是的,那就是它的全部意思。所以如果您要添加另一个需要序列化/反序列化的属性,只需记住您必须在Serialize/Deserialize方法中添加代码,并将它们保持相同的顺序。有点维护工作,但与Xml序列化相比,性能简直不可同日而语(使用xml反序列化需要几分钟,而使用BinaryReader仅需几秒钟,即使有数十万个字典项)。 - GalacticJello

7
你提到的第四种选择是 二进制文件。虽然听起来很古怪和困难,但在 .NET 中使用序列化 API 确实很容易。
无论你选择二进制还是 XML 文件,你都可以使用相同的序列化 API,尽管你需要使用不同的序列化器。
要进行二进制序列化类,它必须标有 [Serializable] 属性或实现 ISerializable 接口。
你也可以使用类似的方法使用 XML,但接口被称为 IXmlSerializable,而属性则是 [XmlRoot] 和 System.Xml.Serialization 命名空间中的其他属性。
如果你想使用关系型数据库,SQL Server Compact Edition 是免费且非常轻量级,并且基于单个文件。

1
扁平文件不等于文本文件。我认为这应该归类为“扁平文件”。 - Adam Robinson
2
无论您是否处理XML文件,都可以对类进行二进制序列化。 - user195488
2
除非你需要序列化对象可读性较高,否则这是最可靠的方法。它可以将其序列化为一个小文件,并且始终似乎是代码运行速度最快的方式。并且马克是正确的,它似乎很古老和困难,但它实际上一点也不难。并且二进制序列化可以捕获整个对象,甚至是它的私有成员,而 XML 序列化则不能。 - CubanX

7

我刚完成了对当前项目的数据存储编码,以下是我的建议。

我开始使用二进制序列化。但是它速度很慢(加载100,000个对象需要约30秒),而且在磁盘上创建的文件也相当大。然而,只需几行代码就可以实现,满足了我所有的存储需求。 为了获得更好的性能,我转向自定义序列化。在 Code Project 上找到了 Tim Haynes 的 FastSerialization 框架。确实比二进制序列化快几倍(加载需要12秒,保存需要8秒,100K 记录),并且占用的磁盘空间更少。该框架基于 GalacticJello 在之前的帖子中概述的技术构建。

然后我转向 SQLite,并能够获得2到3倍的性能提升-加载和保存100K 记录分别需要6秒和4秒。它包括将 ADO.NET 表解析为应用程序类型。它还使我在磁盘上获得了更小的文件。这篇文章解释了如何从 ADO.NET 中获得最佳性能:http://sqlite.phxsoftware.com/forums/t/134.aspx。生成 INSERT 语句是一个非常糟糕的想法。你可以猜到我是怎么知道这个的。:) 的确,SQLite 的实现花费了我相当长的时间,还需要仔细地测量几乎每一行代码所花费的时间。


5

我首先会查看数据库。然而,序列化是一种选择。如果您选择二进制序列化,那么我建议避免使用BinaryFormatter - 如果您更改字段等内容,则它很容易在版本之间出现问题。通过XmlSerializer进行的Xml序列化将是不错的选择,并且可以与protobuf-net同时使用(即使用相同的类定义)进行侧面兼容的二进制序列化(使您获得一个平面文件序列化器而不需要任何努力)。


4
如果您的数据复杂、数量大或需要在本地查询,则对象数据库可能是一个有效的选择。我建议看看Db4oKarvonite

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接