79

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

如果我使用平面文件,这将相当简单,因为数据实际上并不需要保护(它只会存储在这台 PC 上)。因此,我相信的选择是:

  • 平面文件
  • XML
  • 数据库

平面文件需要更多的努力来维护(没有像 XML 这样的内置类),但是我以前没有使用过 XML,对于这个相对简单的任务,SQL 似乎有点过分了。

还有其他值得探索的途径吗?如果不是,哪些是最好的解决方案?


编辑:要为问题添加更多数据,基本上我唯一想要存储的是一个看起来像这样的字典

Dictionary<string, List<Account>> 

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

我会将dict序列化为xmlroot,然后将Account类型序列化为属性吗?


更新 2:

所以可以序列化一个字典。让它变得复杂的是,这个 dict 的值本身就是一个泛型,它是一个 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>

如您所见,继承权是

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

从 a 获得这个布局Dictionary<Username, List<Account>>是一个棘手的问题,也是这个问题的本质。

这里有很多关于序列化的“如何”回复,这是我的错,因为我没有在早期把它说得更清楚,但现在我正在寻找一个明确的解决方案。

4

19 回答 19

28

我将文件存储为JSON。由于您存储的字典只是一个名称/值对列表,因此这几乎就是 json 的设计目的。
有很多不错的免费 .NET json 库 - 这里有一个,但您可以在第一个链接上找到完整列表。

于 2009-12-21T20:19:27.500 回答
24

这实际上取决于您存储的内容。如果您谈论的是结构化数据,那么 XML 或非常轻量级的 SQL RDBMS(如 SQLite 或 SQL Server Compact Edition)都适合您。如果数据超出微不足道的大小,SQL 解决方案就会变得特别引人注目。

如果您要存储大量相对非结构化的数据(例如图像等二进制对象),那么显然数据库和 XML 解决方案都不合适,但考虑到您的问题,我猜前者比后者更多。

于 2009-12-21T19:03:25.097 回答
18

以上都是很好的答案,一般都能解决问题。

如果您需要一种简单、免费的方式来扩展到数百万条数据,请尝试GitHubNuGet上的 ESENT 托管接口项目。

ESENT 是一个嵌入式数据库存储引擎 (ISAM),它是 Windows 的一部分。它通过行级锁定、预写日志记录和快照隔离提供可靠、事务处理、并发、高性能的数据存储。这是 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();
    }

回答乔治的问题:

支持的键类型

仅支持这些类型作为字典键:

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

支持的值类型

字典值可以是任何键类型、键类型的 Nullable 版本、Uri、IPAddress 或可序列化的结构。一个结构只有在满足所有这些条件时才被认为是可序列化的:

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

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

可以序列化 [Serializable] struct Good { public DateTime? 已收到; 公共字符串名称;公开十进制价格;公共 Uri 网址;}

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

于 2009-12-21T19:41:56.550 回答
15

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

另请参阅如何决定在哪里存储每个用户的状态?注册表?应用程序数据?隔离存储?

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();
    }
}
于 2009-12-21T19:02:54.400 回答
9

我推荐文件的 XML 读取器/写入器类,因为它很容易序列化。

C#中的序列化

序列化(在 python 中称为酸洗)是将对象转换为二进制表示的简单方法,然后可以将其写入磁盘或通过线路发送。

它很有用,例如,可以轻松地将设置保存到文件中。

如果你用[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 等调用配置文件的参数。

于 2009-12-21T19:07:01.753 回答
9

如果您的集合太大,我发现 Xml 序列化会变得很慢。序列化字典的另一个选项是使用 BinaryReader 和 BinaryWriter“自己动手”。

这是一些示例代码,只是为了帮助您入门。您可以使这些通用扩展方法来处理任何类型的字典,它工作得很好,但过于冗长,无法在此处发布。

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;
    }
}
于 2009-12-21T20:52:27.690 回答
7

您提到的第四个选项是二进制文件。尽管这听起来晦涩难懂,但使用 .NET 中的序列化 API 确实很容易。

无论您选择二进制文件还是 XML 文件,都可以使用相同的序列化 API,尽管您会使用不同的序列化程序。

要对类进行二进制序列化,必须使用 [Serializable] 属性对其进行标记或实现 ISerializable。

您可以使用XML执行类似的操作,尽管该接口称为 IXmlSerializable,并且属性是 [XmlRoot] 和 System.Xml.Serialization 命名空间中的其他属性。

如果您想使用关系数据库,SQL Server Compact Edition是免费的并且非常轻量级,并且基于单个文件。

于 2009-12-21T19:04:10.850 回答
7

刚刚完成了我当前项目的编码数据存储。这是我的 5 美分。

我从二进制序列化开始。它很慢(加载 100,000 个对象大约需要 30 秒),而且它还在磁盘上创建了一个相当大的文件。但是,我花了几行代码来实现,并且满足了我所有的存储需求。为了获得更好的性能,我转向了自定义序列化。Tim Haynes 在 Code Project 上找到了 FastSerialization 框架。事实上,它快了几倍(加载时间为 12 秒,保存时间为 8 秒,记录 10 万条记录)并且占用的磁盘空间更少。该框架建立在 GalacticJello 在前一篇文章中概述的技术之上。

然后我转移到 SQLite 并且能够获得 2 有时 3 倍更快的性能——加载 6 秒,保存 4 秒,100K 记录。它包括将 ADO.NET 表解析为应用程序类型。它还给了我磁盘上小得多的文件。本文解释了如何从 ADO.NET 中获得最佳性能:http: //sqlite.phxsoftware.com/forums/t/134.aspx。生成 INSERT 语句是一个非常糟糕的主意。你可以猜到我是怎么知道这件事的。:) 事实上,SQLite 的实现花费了我相当多的时间,而且还仔细测量了几乎每一行代码所花费的时间。

于 2009-12-23T20:34:23.290 回答
5

我首先要看的是数据库。但是,序列化是一种选择。如果您进行二进制序列化,那么我会避免 BinaryFormatter- 如果您更改字段等,它倾向于在版本之间生气。Xml viaXmlSerialzier会很好,并且可以并排兼容(即具有相同的类定义)如果你想尝试基于契约的二进制序列化(给你一个平面文件序列化器,不费吹灰之力),可以使用 protobuf-net。

于 2009-12-21T20:35:50.750 回答
4

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

于 2009-12-21T19:40:06.243 回答
3

该线程中的许多答案都试图过度设计解决方案。如果我是正确的,您只想存储用户设置。

为此,请使用 .ini 文件或 App.Config 文件。

如果我错了,并且您存储的数据不仅仅是设置,请使用 csv 格式的纯文本文件。这些操作既快速又简单,没有 XML 的开销。人们喜欢大便这些,因为它们不那么优雅,不能很好地扩展并且在简历上看起来也不那么好,但根据您的需要,它可能是您的最佳解决方案。

于 2009-12-21T19:37:50.223 回答
2

在不知道您的数据是什么样子的情况下,即复杂性、大小等...XML 易于维护和访问。我不会使用 Access 数据库,而且从长远来看,平面文件更难维护,特别是如果您要处理文件中的多个数据字段/元素。

我每天处理大量的大型平面文件数据馈送,即使是一个极端的例子,平面文件数据也比我处理的 XML 数据馈送更难维护。

使用 C# 将 XML 数据加载到数据集的简单示例:

DataSet reportData = new DataSet();

reportData.ReadXml(fi.FullName);

您还可以查看 LINQ to XML 作为查询 XML 数据的选项...

HTH...

于 2009-12-21T19:15:53.980 回答
1

我已经完成了几个具有本地数据存储的“独立”应用程序。我认为最好使用的是 SQL Server Compact Edition(以前称为 SQLAnywhere)。

它轻巧且免费。此外,您可以坚持编写可在其他项目中重用的数据访问层,而且如果应用程序需要扩展到更大的东西,例如完整的 SQL 服务器,您只需要更改连接字符串。

于 2009-12-21T19:17:15.947 回答
1

根据您的 Account 对象的复杂性,我会推荐 XML 或平面文件。

如果每个帐户只存储几个值,您可以将它们存储在属性文件中,如下所示:

account.1.somekey=Some value
account.1.someotherkey=Some other value
account.1.somedate=2009-12-21
account.2.somekey=Some value 2
account.2.someotherkey=Some other value 2

……等等。从属性文件中读取应该很容易,因为它直接映射到字符串字典。

至于在哪里存储这个文件,最好的选择是存储到 AppData 文件夹中,在您的程序的子文件夹中。这是当前用户始终有权写入的位置,并且操作系统本身会保护它不受其他用户的影响。

于 2009-12-21T19:44:39.527 回答
0

我的第一个倾向是访问数据库。.mdb 文件存储在本地,如果认为有必要,可以对其进行加密。尽管 XML 或 JSON 也适用于许多场景。我只会将平面文件用于只读、非搜索(前向只读)信息。我倾向于更喜欢 csv 格式来设置宽度。

于 2009-12-21T19:03:33.757 回答
0

这取决于您要存储的数据量。实际上,平面文件和 XML 之间没有区别。XML 可能更可取,因为它为文档提供了结构。在实践中,

最后一个选项,现在很多应用程序使用的是 Windows 注册表。我个人不推荐它(注册表膨胀、腐败、其他潜在问题),但它是一种选择。

于 2009-12-21T19:06:18.680 回答
0

如果您采用二进制序列化路线,请考虑需要访问数据的特定成员的速度。如果它只是一个小集合,加载整个文件是有意义的,但如果它很大,你也可以考虑一个索引文件。

跟踪位于文件中特定地址的帐户属性/字段可以帮助您加快访问时间,尤其是在您根据密钥使用优化该索引文件时。(甚至可能在您写入磁盘时。)

于 2009-12-21T19:37:07.353 回答
0

保持简单 - 正如你所说,一个平面文件就足够了。使用平面文件。

这是假设您已经正确分析了您的需求。我会跳过序列化为 XML 的步骤,这对于一个简单的字典来说太过分了。数据库也是如此。

于 2009-12-21T20:23:48.100 回答
0

根据我的经验,在大多数情况下,文件中的 JSON 就足够了(大多数情况下,您需要存储一个数组或一个对象,或者只是一个数字或字符串)。我很少需要 SQLite(它需要更多时间来设置和使用它,大多数时候它是矫枉过正的)。

于 2017-05-12T07:05:15.347 回答