在软件开发中,性能不仅仅是一种奢侈品,更是一种必需品。作为高级开发人员,我们经常面临为我们的任务选择最合适的数据结构的关键决策。List**<T>**、Dictionary<TKey、TValue> 和 HashSet<T> 之间的选择可以决定应用程序的性能,尤其是在大规模运行时。本文旨在为您提供做出明智决策所需的见解,并以性能指标和实际场景为后盾
我们将探讨何时使用每种 Token、它们的性能特征以及如何优化它们以实现最高效率
想象一下一把瑞士军刀。多才多艺,对吧?这就是我们的清单!当您需要所有东西时,它是首选工具。
非常适合需要有序集合和频繁位置访问的方案
考虑一个顺序至关重要的任务管理系统:
public class Task
{
public string Description { get; set; }
public bool IsCompleted { get; set; }
}
public class ToDoList
{
private List<Task> tasks = new List<Task>();
public void AddTask(string description)
{
tasks.Add(new Task { Description = description, IsCompleted = false });
}
public void CompleteTask(int index)
{
if (index >= 0 && index < tasks.Count)
{
tasks[index].IsCompleted = true;
}
}
public List<Task> GetIncompleteTasks()
{
return tasks.Where(t => !t.IsCompleted).ToList();
}
}
在后台,List 使用动态数组。它就像一个神奇的扩展手提箱——它会随着您添加更多物品而增长。但这是关键所在!
当它增长时,它不仅会再增加一个槽位。它的大小翻了一番! 💨
这意味着添加项目通常非常快 (O(1)),但有时当它需要增长时,需要更长的时间 (O(n))。不过,平均而言,它仍然相当活泼!
🪶 优化查找速度
当基于键的快速检索成为优先事项时,Dictionary<TKey, TValue> 成为一种强大的解决方案。
需要找点东西吗?干掉!你立刻就到了。🌠
在高流量应用程序中,高效的用户配置文件检索至关重要。假设我们正在为我们的社交媒体应用程序构建一个用户档案系统。我们希望通过用户的 ID 以超快的速度访问用户资料。
public class UserProfile
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
}
public class UserProfileCache
{
private Dictionary<int, UserProfile> profileCache = new Dictionary<int, UserProfile>();
public void AddOrUpdateProfile(UserProfile profile)
{
profileCache[profile.Id] = profile;
}
public UserProfile GetProfile(int userId)
{
return profileCache.TryGetValue(userId, out var profile) ? profile : null;
}
public bool ProfileExists(int userId)
{
return profileCache.ContainsKey(userId);
}
}
Dictionary<TKey, TValue> 在后台使用哈希表。可以把它想象成一个超级有序的图书馆,其中每本书 (value) 都有一个唯一的索书号 (key)。图书管理员(哈希函数)几乎可以立即告诉您在哪里可以找到任何书籍!
为基于键的操作启用 O(1) 平均情况时间复杂度
🪶 高效的唯一性和集合操作
想象一下,在一家夜总会里,每个人都必须是独一无二的。这就是 HashSet。它是确保没有重复项进入的保镖。🚫👥
它在维护唯一元素集合至关重要的情况下表现出色
让我们构建一个系统来跟踪我们的社交媒体应用程序中使用的独特主题标签。
public class HashtagTracker
{
private HashSet<string> uniqueHashtags = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
public bool AddHashtag(string hashtag)
{
return uniqueHashtags.Add(hashtag);
}
public bool HasBeenUsed(string hashtag)
{
return uniqueHashtags.Contains(hashtag);
}
public int UniqueHashtagCount => uniqueHashtags.Count;
public IEnumerable<string> GetCommonHashtags(HashtagTracker other)
{
return uniqueHashtags.Intersect(other.uniqueHashtags, StringComparer.OrdinalIgnoreCase);
}
}
HashSet 的构建原理与 Dictionary<TKey、TValue>相同,但它只存储键,不存储值
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Generic;
[MemoryDiagnoser]
public class DataStructureBenchmarks
{
private const int N = 1_000_000;
private List<int> _list;
private Dictionary<int, bool> _dict;
private HashSet<int> _set;
[GlobalSetup]
public void Setup()
{
_list = new List<int>(N);
_dict = new Dictionary<int, bool>(N);
_set = new HashSet<int>(N);
var random = new Random(42);
for (int i = 0; i < N; i++)
{
var value = random.Next();
_list.Add(value);
_dict[value] = true;
_set.Add(value);
}
}
[Benchmark]
public bool ListContains() => _list.Contains(N / 2);
[Benchmark]
public bool DictContains() => _dict.ContainsKey(N / 2);
[Benchmark]
public bool SetContains() => _set.Contains(N / 2);
[Benchmark]
public void ListAdd() => _list.Add(N + 1);
[Benchmark]
public void DictAdd() => _dict[N + 1] = true;
[Benchmark]
public void SetAdd() => _set.Add(N + 1);
}
public class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<DataStructureBenchmarks>();
}
}
| Method | Mean | Error | StdDev | Gen 0 | Allocated |
|------------ |----------------:|--------------:|--------------:|--------:|----------:|
| ListContains | 2,924,483.95 ns | 57,638.858 ns | 91,235.298 ns | - | 40 B |
| DictContains | 22.36 ns | 0.479 ns | 0.787 ns | - | - |
| SetContains | 19.84 ns | 0.424 ns | 0.635 ns | - | - |
| ListAdd | 27.59 ns | 0.589 ns | 0.918 ns | 0.0153 | 32 B |
| DictAdd | 31.84 ns | 0.681 ns | 1.061 ns | 0.0153 | 32 B |
| SetAdd | 31.47 ns | 0.673 ns | 1.048 ns | 0.0153 | 32 B |
对于 Lookup 操作Dictionary<TKey、TValue> 和 HashSet<T> 表现出卓越的性能,时间复杂度几乎恒定。List<T> 表现出线性的时间复杂度,使其不太适合大规模查找操作。
**对于 Insertion Operations (插入操作),**所有三种结构在单次插入时都显示出相当的性能。但是,由于内部数组大小调整,List<T> 可能会偶尔遇到性能影响。
对于内存利用率List<T> 被证明是每个元素的内存效率最高的。Dictionary<TKey、TValue> 和 HashSet<T> 用一些内存效率换取了它们在查找操作中的速度优势。
好吧,帅哥们!🔥 您已经掌握了基础知识,现在让我们通过一些高级技术将其提升一个档次
有时,您需要 Dictionary<TKey>TValue 或 HashSet 以特殊方式比较事物。因此,请实现 IEqualityComparer<T> 来定义自定义相等性和哈希代码生成:
public class CaseInsensitiveComparer : IEqualityComparer<string>
{
public bool Equals(string x, string y) => string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
public int GetHashCode(string obj) => obj.ToLowerInvariant().GetHashCode();
}
var uniqueStrings = new HashSet<string>(new CaseInsensitiveComparer());
uniqueStrings.Add("CSharp");
Console.WriteLine(uniqueStrings.Contains("csharp")); // True
当您知道要添加大量项目时,请提前提醒您的数据结构:
var bigList = new List<int>(1_000_000); // Preallocate for better performance
var bigDict = new Dictionary<int, string>(1_000_000);
此策略可最大程度地减少重新分配开销,尤其适用于大型集合。
3. ConcurrentDictionary<TKey, TValue>:线程安全操作
对于多线程方案
private ConcurrentDictionary<int, UserProfile> _userCache = new ConcurrentDictionary<int, UserProfile>();
public UserProfile GetOrAddUser(int userId, Func<int, UserProfile> createProfile)
{
return _userCache.GetOrAdd(userId, createProfile);
}
让我们运用我们的知识为社交媒体平台设计一个高性能的后端:
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public HashSet<int> Followers { get; set; } = new HashSet<int>();
public HashSet<int> Following { get; set; } = new HashSet<int>();
}
public class Post
{
public int Id { get; set; }
public int UserId { get; set; }
public string Content { get; set; }
public DateTime Timestamp { get; set; }
}
public class SocialMediaBackend
{
private Dictionary<int, User> _users = new Dictionary<int, User>();
private List<Post> _posts = new List<Post>();
private Dictionary<string, HashSet<int>> _hashtags = new Dictionary<string, HashSet<int>>(StringComparer.OrdinalIgnoreCase);
public void AddUser(User user)
{
_users[user.Id] = user;
}
public void AddPost(Post post)
{
_posts.Add(post);
// Extract and index hashtags
foreach (var word in post.Content.Split())
{
if (word.StartsWith("#"))
{
if (!_hashtags.TryGetValue(word, out var postIds))
{
postIds = new HashSet<int>();
_hashtags[word] = postIds;
}
postIds.Add(post.Id);
}
}
}
public IEnumerable<Post> GetUserFeed(int userId)
{
if (_users.TryGetValue(userId, out var user))
{
return _posts.Where(p => user.Following.Contains(p.UserId))
.OrderByDescending(p => p.Timestamp);
}
return Enumerable.Empty<Post>();
}
public IEnumerable<Post> GetPostsByHashtag(string hashtag)
{
if (_hashtags.TryGetValue(hashtag, out var postIds))
{
return _posts.Where(p => postIds.Contains(p.Id))
.OrderByDescending(p => p.Timestamp);
}
return Enumerable.Empty<Post>();
}
}
数据结构的选择会显著影响应用程序的性能和可伸缩性。作为高级开发人员,我们的责任是根据每个场景的具体要求和约束做出明智的决策。