目录
介绍
使用SortedBucketCollection
创建一个SortedBucketCollection
将项添加到SortedBucketCollection
遍历SortedBucketCollection中的所有项
访问一个特定项
移除项
访问一天的所有项
SortedBucketCollection的实现
真的需要一个新的集合吗?
泛型参数是什么?
要实现哪些接口?
内部数据结构
需要哪些属性?
需要哪些方法?
如何实现IEnumerable<>
多线程安全
单元测试
SortedBucketCollection与SortedDirectory的性能
SortedBucketCollection的代码
最大优势
下载源代码 - 3.7 KB
我正在编写一个供自己使用的会计软件,并且需要每天存储一些财务交易。存储它们的一种简单方法是Dictionary
我改写了SortedBucketCollection
对于我的财务应用程序,我这样使用它:SortedBucketCollection
本文涵盖了几个方面:
以下是如何使用SortedBucketCollection的完整代码:
using System;
namespace SortedBucketCollectionDemo {
public record FinanceTransaction
(int No, DateTime Date, string Description, decimal Amount);
class Program {
static void Main(string[] args) {
//Constructing a SortedBucketCollection
var transactions =
new SortedBucketCollection
(ft=>ft.Date, ft=>ft.No);
var date1 = DateTime.Now.Date;
//Adding an item to SortedBucketCollection
transactions.Add(new FinanceTransaction(3, date1, "1.1", 1m));
transactions.Add(new FinanceTransaction(1, date1, "1.2", 2m));
transactions.Add(new FinanceTransaction(0, date1, "1.3", 3m));
var date2 = date1.AddDays(-1);
transactions.Add(new FinanceTransaction(1, date2, "2.1", 4m));
transactions.Add(new FinanceTransaction(2, date2, "2.2", 5m));
//Looping over all items in a SortedBucketCollection
Console.WriteLine("foreach over all transactions");
foreach (var transaction in transactions) {
Console.WriteLine(transaction.ToString());
}
//Accessing one particular transaction
var transaction12 = transactions[date1, 1];
//Removing a transaction
transactions.Remove(transaction12!);
//Accessing all items of one day
Console.WriteLine();
Console.WriteLine("foreach over transactions of one day");
Console.WriteLine(date1);
foreach (var transaction in transactions[date1]) {
Console.WriteLine(transaction.ToString());
}
}
}
}
SortedBucketCollection类的定义:
public class SortedBucketCollection:
ICollection, IReadOnlySortedBucketCollection
where TKey1 : notnull, IComparable
where TKey2 : notnull, IComparable
where TValue : class {
SortedBucketCollection支持两个键。第一个用于将TValue类型的存储项分组在一起。第二个键用于唯一标识该组(存储桶)中的项目。
SortedBucketCollection支持ICollection并可用作任何其他集合。不幸的是,无法实现IList<>,它允许根据项目在列表中的位置访问项目。SortedBucketCollection无法有效地实现这一点。
SortedBucketCollection的构造函数如下所示:
public SortedBucketCollection(Func getKey1, Func getKey2) {}
它有两个参数,代表getKey1和getKey2。我不喜欢SortedList<>和SortedList<>中键和项都需要传递,因为大多数时候,键已经是项的一个属性。为了改进这一点,我添加了这两个允许查找Key1和Key2在项目中的委托。
使用构造函数:
var transactions =
new SortedBucketCollection (ft=>ft.Date, ft=>ft.No)
在演示中,FinanceTransaction有一个日期和一个交易编号,它们用作两个必需的键。
public record FinanceTransaction(int No, DateTime Date, string Description, decimal Amount);
添加项变得非常简单:
transactions.Add(new FinanceTransaction(uniqueNo++, date, "Some remarks", 10.00m));
不需要将键作为单独的参数给出。SortedBucketCollection已经知道如何找到键了。它采用第一个键、一个日期并检查是否已经有任何具有相同日期的项。如果没有,它只会在该日期下添加新项。如果已经有一个,SortedBucketCollection则检查新项应该在现有项之前还是之后。在Add()完成后,将这两项存储在正确排序的链表中。
注意:SortedBucketCollection只需要增加单个SortedDirectory的存储容量,这种情况很少发生。使用这种Dictionary
> 方法,数以千计的列表必须不断增加它们的容量(即,将内部数组复制到更大的数组中)。
与任何其他集合一样,这里没有什么特别之处,SortedBucketCollection支持IEnumerable(TValue):
foreach (var transaction in transactions) {
Console.WriteLine(transaction.ToString());
}
输出如下:
FinanceTransaction { No = 1, Date = 07.11.2021 00:00:00, Description = 2.1, Amount = 4 }
FinanceTransaction { No = 2, Date = 07.11.2021 00:00:00, Description = 2.2, Amount = 5 }
FinanceTransaction { No = 0, Date = 08.11.2021 00:00:00, Description = 1.3, Amount = 3 }
FinanceTransaction { No = 1, Date = 08.11.2021 00:00:00, Description = 1.2, Amount = 2 }
FinanceTransaction { No = 3, Date = 08.11.2021 00:00:00, Description = 1.1, Amount = 1 }
这些项不在添加时的顺序中,就像List<>的情况一样,而是首先按日期排序,然后按No排序。
var transaction = transactions[new DateTime(2021, 11, 07), 1];
transactions.Remove(transaction);
在Dictionary<>中,人们将使用键来删除该项目。SortedBucketCollection可以根据给定的项找出键。
var date1Transaction1 = transactions[date1];
这是SortedBucketCollection真正闪耀的地方。可以将所有交易存储在一个List<>中,并使用Linq查找具有特定日期的所有交易,然后根据transaction.No对它们进行排序 。这将是一个相当耗时的操作。在编写我的财务应用程序时,我发现我需要经常访问每天的交易,这就是我写SortedBucketCollection的原因。
很少有人需要写自己的集合,这意味着这是一项没有多少人具备的技能。在本章中,我将展示编写集合时必须考虑的所有重要方面。当然,这需要写很多细节。如果你觉得这很无聊,请停止阅读这里。
DotNet和Github已经提供了大量的集合。如果有一个可以满足您的需求,请检查Stackoverflow。
在SortedBucketCollection的例子中,我发现了两个建议:
.NET中的大多数集合只有1个键,但我想要2个键,一个用于将值组合在一起,另一个用于标识该组中的一个特定项。如果需要删除单个项,则需要这样做。
public class SortedBucketCollection:
ICollection, IReadOnlySortedBucketCollection
where TKey1 : notnull, IComparable
where TKey2 : notnull, IComparable
where TValue : class {
键用于排序,因此它们需要实现IComparable并且不能是null。
我更喜欢TValue成为一个类,而不是一个避免不必要地复制其内容的struct。
我本来想实现类似IDictionary
您可能已经注意到,我编写了一个新的接口IReadOnlySortedBucketCollection
public IReadOnlySortedBucketCollection Ledgers => ledgers;
readonly SortedBucketCollection ledgers;
所有数据都存储在一个集合SortedDirectory中,其中包含BucketItems:
private class BucketItem {
public readonly TValue Item;
public BucketItem? Next;
public BucketItem(TValue item, BucketItem? next) {
Item = item;
Next = next;
}
}
BucketItem有一个TValue类型的Item,它是实际存储的数据,并且有一个链接到下一个具有相同Key1但不同Key2的BucketItem。请注意,实际上没有Bucket对象。
readonly SortedDictionary buckets;
buckets为每个唯一的TKey1值保存一个BucketItem,如果有几个项目具有相同的TKey1值,则该buckets是链表的头。
IList需要Count。它告诉存储了多少项而不枚举所有项。该IsReadOnly接口也需要,我将其始终设置为false。我添加了Key1Count,它告诉我们使用了多少种不同Key1的东西。
SortedBucketCollection提供两个索引:
如果你的集合实现了一个接口,VS会为你添加所有需要的方法。就我而言,它们是:
ICollection要求:
public void Add(TValue item) {'''}
public bool Remove(TValue item) {
public void Clear() {
public bool Contains(TValue item) {...}
public void CopyTo(TValue[] array, int arrayIndex) {...}
有时,接口要求有某种我们可能不想用于我们的集合的方法。我们不能把它扔掉,但我们可以抛出new NotSupportedException();正如我对CopyTo()所做的。
IEnumerable要求:
public IEnumerator GetEnumerator() {...}
我添加的灵感来自IDictionary:
public bool Contains(TKey1 key1) {...}
public bool Contains(TKey1 key1, TKey2 key2) {..}
IEnumerable<>定义如下:
public interface IEnumerable: IEnumerable {
IEnumerator GetEnumerator();
}
public interface IEnumerator: IEnumerator, IDisposable {
T Current { get; }
}
public interface IEnumerator {
object Current { get; }
bool MoveNext();
void Reset();
}
在.NET的早期,IEnumerable集合的实现很复杂,需要编写一个额外的类,例如SortedBucketCollectionEnumerator,其实现IEnumerator
随着C#关键字的引入yield,事情变得更容易了,但仍然相当混乱,因为人们只能使用yield来创建方法返回IEnumerator
下面是如何编写一个GetEnumerator()实现的代码:
public IEnumerator GetEnumerator() {
var versionCopy = version;
foreach (var keyValuePairBucketItem in buckets) {
var bucketItem = keyValuePairBucketItem.Value;
yield return bucketItem.Item;
if (versionCopy!=version) {
throw new InvalidOperationException();
}
while (bucketItem.Next is not null) {
bucketItem = bucketItem.Next;
yield return bucketItem.Item;
if (versionCopy!=version) {
throw new InvalidOperationException();
}
}
}
}
请注意,它GetEnumerator()返回一个IEnumerator
编译器将GetEnumerator()的主体翻译成一个新的隐藏类,我们来命名它为SortedBucketCollectionEnumerator。它实现IEnumerator
基本上,我的GetEnumerator()包含两个循环:
foreach (var keyValuePairBucketItem in buckets) {
var bucketItem = keyValuePairBucketItem.Value;
while (bucketItem.Next is not null) {
}
}
一个集合通常只需要一个循环,枚举所有键。由于SortedBucketCollection使用两个键,因此需要先循环遍历每个TKey1,然后在该循环中遍历存储桶中的每个TKey2。
对于找到的每个键,都会编写以下语句:
yield return item;
SortedBucketCollection.GetEnumerator()像这样被消费:
foreach (var transaction in transactions) {
Console.WriteLine(transaction);
}
C#编译器将其翻译成如下内容:
using(var sortedBucketCollectionEnumerator = transactions.GetEnumerator()) {
while (sortedBucketCollectionEnumerator.MoveNext()) {
Console.WriteLine(sortedBucketCollectionEnumerator.Current);
}
}
当sortedBucketCollectionEnumerator.MoveNext()第一次被调用时,它会运行基于SortedBucketCollection.GetEnumerator()主体生成的代码,直到找到第一个yield语句,然后停止并返回该项。当sortedBucketCollectionEnumerator.MoveNext()再次被调用时,它会在语句之后继续下一行yield并再次运行直到下一个yield,停在那里并返回该项。重复此操作,直到找不到更多yield语句,然后foreach完成。
.NET集合通常不是多线程安全的,这意味着两个线程不能同时操作集合。但是它们通常会检测一个线程是否正在枚举集合,而另一个或同一个线程是否同时更改集合的内容。这是通过version字段完成的。每次添加或删除项目时,version都会递增。正如您在GetEnumerator()中看到的,在开始时保存当前version,在每次yield之后,有一个测试来确保version仍然具有相同的值。
对于您编写的大多数代码,您还应该编写单元测试。在VS中,将一个MSTest Test Project添加到您的解决方案中。请谷歌如何在VS中编写单元测试,只需记住调用每个方法并在每次调用后检查所有属性是否具有预期值。
编码和测试完成后,应该进行一些性能测试。如何做到这一点,请参阅:benchmarkdotnet.org
在这个例子中,SortedBucketCollection
| Method | Time | Memory |
|------------- |------------:|------------:|
| DataOnly | 233.9 us | - |
| SortBuckets | 10,108.9 us | 2,352,161 B |
| Dictionaries | 12,530.6 us | 4,536,112 B |
第一行DataOnly是一个测试,它只遍历所有已创建的项。必须为发布编译测试,这意味着只读取项的循环可能会优化为根本不读取。为了确保编译器无法做到这一点,我总结了所有item.Key2。由于此测试只是读取,因此不需要堆内存。
SortBuckets和Dictionaries之间的速度差异只有20%,我希望我的集合会更快。但SortBuckets只需要Dictionaries一半的内存。
我想提高SortedBucketCollection速度的一种方法是使BucketItem是struct而不是class,将它们放入一个大数组中,该数组会像List<>内部一样随着时间的推移而增长,并重用不再需要的BucketItems,这将消除在上面的测试中约100 000对象创建。不过目前的速度还可以接受,所以没有花更多的时间在速度提升上。
我已经附上了这篇文章SortedBucketCollection的源代码。它基本上是一个包含SortedBucketCollection的文件和另一个显示如何使用它的文件:
你也可以在Github上找到SortedBucketCollection源代码:
Storage是我为单用户应用程序编写的一个库,用于将任何数据库替换为以下存储系统:
开发人员需要编写更少的代码,一切都在C#中。不需要使用像有时很慢的实体框架或任何数据库管理这样的东西。任何编辑器或Excel都可以轻松读取数据文件,并且数据的维护变得非常容易。多年来,我一直在将Storage用于金融应用程序,从未遇到过任何问题。
如果您需要在本地磁盘上编写高性能、高可靠性和永久数据存储的单用户应用程序,请尝试一下Storage。
https://www.codeproject.com/Articles/5317083/SortedBucketCollection-A-memory-efficient-SortedLi