在.NET(C#)中本地存储数据的最佳方法


75

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

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

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

平面文件需要花费更多的精力来维护(没有像XML这样的内置类),但是我以前从未使用过XML,对于这个相对简单的任务来说,SQL似乎有些过头。

还有其他值得探索的途径吗?如果没有,哪个是最佳解决方案?


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

Dictionary<string, List<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>>是一个棘手的问题,也是这个问题的本质。

关于序列化,这里有很多“如何做”的响应,这是我的错,因为我并没有很早就弄清楚,但是现在我正在寻找一个确定的解决方案。


提供有关应用程序类型和存储的数据的更多细节,以及期望的sizr
AK_09年

3
序列化字典的方法:stackoverflow.com/questions/1111724
Cheeso,

Answers:


26

我将文件存储为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配置文件必须结构化吗?

@Roboto:按照定义,XML是结构化的。但是,这并不意味着您必须以高度结构化的方式使用它们。
亚当·罗宾逊

17

以上所有都是很好的答案,通常可以解决问题。

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

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();
    }

要回答乔治的问题:

支持的密钥类型

仅支持将以下类型用作字典键:

布尔字节Int16 UInt16 Int32 UInt32 Int64 UInt64浮点型双引导DateTime TimeSpan字符串

支持的值类型

字典值可以是任何密钥类型,密钥类型的可空版本,Uri,IPAddress或可序列化的结构。只有满足以下所有条件的结构才被认为是可序列化的:

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

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

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

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


此方法基本上是用持久性字典替换内置的泛型。这是一个非常优雅的解决方案,但是它如何像OP示例中那样处理复杂的对象?它是将所有内容存储在字典中还是仅存储在字典本身中?
乔治,

这可能无法达到保存帐户类型列表的最终目标。密钥是可以的,但是要使通用的可序列化可能很困难:/。
乔治

1
知道您可以使用Nuget来获取ManagedEsent的人可能会从中受益。然后,您需要引用Esent.Collections.DLL和Esent.ISAM.DLL。然后添加“使用Microsoft.Isam.Esent.Collections.Generic;” 获取PersistentDictionary类型。藏品DLL可能必须从下载选项上下载managedesent.codeplex.com
史蒂夫-希伯特

“ ManagedEsent”已变为“ Microsoft.Database.ManagedEsent”。您应该改用nuget中的“ Microsoft.Database.Collections.Generic”,因为它同时包含ManagedEsent和ISAM。
VoteCoffee

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();
    }
}

1
这不是很便携也不整洁

3
它看起来像剪切粘贴代码,因此您可以成为第一个张贴者。坚持使用您的链接会更好。

9
好吧,是的,我将它从我编写的应用程序中删除了,就可以了。Roboto,您的问题是什么?
Cheeso

它甚至不使用依赖注入!你没收到备忘录吗?
史蒂夫·史密斯

我同意XML的建议,但是我自己尝试一下之后,遇到了隔离存储的限制-它会根据运行代码的程序集创建不同的文件。最终仅使用AppData \ Roaming通过Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"{AppName}\{FileName}.XML");
Ory Zaidenvorm

9

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

C#中的序列化

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

例如,将设置轻松保存到文件中非常有用。

如果将它们标记为[Serializable] attribute,则可以序列化自己的类。这会序列化一个类的所有成员,但标记为的成员除外 [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序列化使用反射来生成特殊的类来执行序列化。

如果代码缩进而没有随机大小,则在读取代码时会有所帮助...
Lasse V. Karlsen 2009年

@Lasse:不确定您的意思,但是如果很难阅读,则可以对其进行编辑

8

如果您的收藏太大,我发现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;
    }
}

谢谢,这似乎是迄今为止最好的解决方案。与反序列化/序列化保持同步是什么意思?如在更新文件时对其进行修改?此功能仅在应用程序启动和退出时使用,以保存字典,所以请您澄清一下吗?否则,非常感谢。
乔治

经过一番思考,我意识到它意味着序列化和反序列化的逻辑应该是相同的。就这些。
乔治,

是的,仅此而已。因此,如果您添加另一个属性来进行序列化/反序列化,请记住,您必须将代码添加到Serialize / Deserialize方法中,并保持它们的顺序相同。有点维护,但是在Xml序列化上的性能是无可比拟的(使用xml反序列化需要几分钟,使用BinaryReader需要几十万个字典项目需要几秒钟)。
GalacticJello

谢谢,这是一个完美的解决方案
Sarath Vuyyuru 2012年

7

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

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

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

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

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


1
平面文件!=文本文件。我认为这将属于“平面文件”类别。
亚当·罗宾逊

2
无论您是否在处理XML文件,都可以对一个类进行二进制序列化

2
除非您需要序列化的对象是人类可读的,否则这是最可靠的方法。它序列化为一个小文件,并且就代码运行的速度而言,它始终似乎是最快的方法。马克说的没错,这看起来很神秘而且很困难,但事实并非如此。二进制序列化捕获整个对象,甚至是其私有成员,而XML序列化则没有。
CubanX

6

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

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

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


5

我要看的第一件事是数据库。但是,可以选择序列化。如果您要进行二进制序列化,那么我会避免 BinaryFormatter-如果您更改字段等,它XmlSerialzier会在版本之间惹恼。Xml via可以,并且可以并排兼容(即,具有相同的类定义)如果您想尝试基于合约的二进制序列化,请使用protobuf-net(不费吹灰之力即可获得平面文件序列化器)。


4

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


3

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

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

如果我错了,并且您存储的数据不只是设置,请使用csv格式的纯文本文件。这些操作快速简便,而没有XML的开销。人们不喜欢这些便笺,因为它们不那么优雅,缩放性不佳,履历表看起来也不那么好,但是根据您的需要,这可能是最适合您的解决方案。


的app.config VS自定义XML:stackoverflow.com/questions/1565898/...

我正在做一些比设置稍微复杂的事情。每个用户可能有多个与他们的姓名相关联的“帐户”。词典将此名称(字符串)链接到与其关联的帐户列表。我会为每个用户存储一堆帐户。它可以与xml一起工作,但是我不太确定如何去做。
乔治,

在那种情况下,我将使用提到的XmlSerializer类。如果您对OOP有很好的了解,那应该很容易。这是一个很好的例子:jonasjohn.de/snippets/csharp/xmlserializer-example.htm
James

2

在不知道数据是什么样的情况下(即复杂性,大小等),XML易于维护且易于访问。我不会使用Access数据库,并且从长远来看,更难以维护平面文件,尤其是当您处理文件中的多个数据字段/元素时。

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

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

DataSet reportData = new DataSet();

reportData.ReadXml(fi.FullName);

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

HTH ...


1

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

它轻巧免费。此外,您可以坚持编写可在其他项目中重用的数据访问层,此外,如果应用程序需要扩展到更大的功能(如功能强大的SQL Server),则只需更改连接字符串即可。


1

根据您Account对象的兼容性,我建议使用XML或Flat文件。

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

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文件夹中。在此位置,当前用户将始终可以进行写操作,并且操作系统本身可以保护它免受其他用户的攻击。


0

我的第一个倾向是访问数据库。.mdb文件存储在本地,并且如果需要可以进行加密。尽管XML或JSON也可以在许多情况下使用。我只将平面文件用于只读,非搜索(正向只读)信息。我倾向于使用csv格式设置宽度。


2
出于好奇,为什么要使用Access,除非它已经存在或需要从Access中访问它?否则,似乎更建议使用轻量级的SQL引擎,尤其是使用进程中选项(如SQLite和SQL Server CE)时。
亚当·罗宾逊

我认为,JET引擎-允许您使用.MDB文件的工具随Windows一起安装,这正是利用.MDB文件作为解决方案的原因,并且如果您愿意的话,它们很容易通过访问进行挖掘。需要。但是,这早于SQL Server CE的当前版本(可以是.DLL“ xcopy”部署),因此是一种实现大致相似结果的更好方法。
Murph,2009年

15
朋友不要让朋友使用Access

@Murph:是的,JET现在是Windows组件,但是(如您所指出的那样)SQL Server CE的XCOPY部署(和进程内托管)似乎消除了JET的任何优点,同时保留了缺点(有限和奇怪) SQL语法支持,许多ORM不支持,等等。所以...我想我的问题仍然是您为什么推荐它:)
亚当·罗宾逊2009年

1
正如我几乎所有的老师所说的那样:Access不是一个真正的数据库:)
Florian K

0

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

最后一个选项以及现在很多应用程序都在使用Windows注册表。我个人不建议这样做(注册表膨胀,腐败等潜在问题),但这是一个选择。


平面文件与XML不同的一个区域表示分层数据。当然,您可以在平面文件中表示层次结构,但是在XML中这样做更容易。
itowlson

0

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

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


0

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

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


0

以我的经验,在大多数情况下,文件中的JSON就足够了(大多数情况下,您需要存储一个数组或一个对象,或者仅存储一个数字或字符串)。我很少需要SQLite(在大多数情况下,SQLite会花很多时间来设置和使用它)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.