C#数据结构使用

199 阅读24分钟

数据结构:计算机存储和组织数据的一种方式,指各种数据类型相互之间的关系

元组

用元组可以少生成一些类,不用单独生成一个类。

元组使用泛型来简化类的定义,多用于方法的返回值。在函数需要返回多个类型的时候,就不必使用out , ref等关键字了,直接定义一个Tuple类型,使用起来非常方便

元组的键名是item1,item2

基本使用

在我们编程时,比如一个人的信息,我们常常创建一个UserInformation类去描述一个人,传统的做法如下


public class UserInformationpublic string FirstName { getset; }
    public string LastName { getset; }
}

static UserInformation GetUserInformation()
{
    return new UserInformation { FirstName = "张三", LastName = "李四" };
}

那么我们使用元组可以怎么做呢?如下所示

static Tuple<stringstringGetUserInformation3()
{
    return new Tuple<string,string> ( "张三""李四" );
}
var tuple = GetUserInformation1();
string firstName=  tuple.Item1;
string lastName = tuple.Item2;

Item1代表第一个属性值,Item2代表第二个属性值,当然如果使用Item1和item2可读性比较差,不看方法根本不知道Item1代表什么意思,这时候我们就可以使用匿名类型了,如下所示

var ( first, last) = GetUserInformation1();

first是我们的第一个属性值,last是我们的第二个属性值。在C#7.0中它提供了更简洁的语法,用于将多个数据元素分组成一个轻型数据结构

static (stringstring) GetUserInformation1()
{
    return  ("张三""李四");
}

命名元组

//声明
Task<(object total, object page)>
//赋值
var s=(total:3,page:2)

读取元组返回值

(var result, var user, var password) = await repository.ResetPasswordAsync(id);

元组的坑

1、不要用dynamic接收,可以用var接收

2、序列化后名字自动变为item1、item2

跟元组的底层实现原理相关

元组的原理

var foo = (name: "lindexi", site: "blog.csdn.net/lindexi_gd");

被编译成下面的代码

[return: TupleElementNames(new string[]
{
    "name",
    "site"
})]

private static ValueTuple<string, string> Foo()
{
    return new ValueTuple<string, string>("lindexi", "blog.csdn.net/lindexi_gd");
}

所以实际上代码是 ValueTuple<string, string> 不是刚才定义的代码,只是通过 TupleElementNames 让编译器知道值,所以是语法糖。

如下,GetBoardDelayData返回命名元组,一个名为full、一个名为simple

//用var接收,可以正常通过名字访问
var dataList = await repository.GetBoardDelayData(req);
var equips = dataList.simple.ToList();

//用dynamic接收,神奇的现象出现了,键名full、simple自动变为item1、item2
dynamic dataList;
dataList= await repository.GetBoardDelayData(req);
dataList.full    //报错,键名full、simple自动变为item1、item2

//解决方案1,用元组接收
dynamic full, simple;
(full,simple) = await repository.GetBoardDelayData(req);

//错误解决方案,用var接收,赋值给dynamic,序列化后,键名full、simple自动变为item1、item2
dynamic dataList;
var res = await repository.GetBoardDelayData(req);
dataList = res;

元组序列化为JSON

即使元组有数据,前端得到的结果可能是空{ }

//令一个函数返回值为元组的写法,前端返回结果为空
public async  Task<(int,Equip[])> FindAll() {
    //...
    var data=await repository.GetEquipsAsync();
    //返回元组的写法
    return (1,data);
}
//也可能可以返回,使用别的对象来包含元组时,元组键名是item1,item2...

元组如何转对象

猜测没有直接转换的方法,只能用传统方法

public record EquipResopnse(long count, long pageCount, Equip[] equips);
//...
( count,pageCount,equips)= await repository.GetEquipsByPageAsync(pageIndex, pageSize,true,"id");  
new EquipResopnse(count, pageCount, equips);

Array

Array 类是一个抽象类,因此不能实例化一个对象来创建数组,平常我们 int[],string[]... 事实上就是声明一个 Array 数组了。

但是可以这样来创建一个数组

Array iArray = new int[] {1,2,3,4,5};
for (int i = 0;i < iArray.Length;i ++)
{
  Console.WriteLine(iArray.GetValue(i));
}

CreateInstance创建数组

1、事先不知道元素类型时,可以这样定义数组

2、该方法也可用于创建多维数组

或者使用静态方法 CreateInstance,尤其是事先不知道数组元素类型的时候,可以这样定义数组。其中 SetValue 方法用于设定,GetValue方法用于读取

Array array = Array.CreateInstance(typeof(string),3);
array.SetValue("Tom",0);
array.SetValue("Jack",1);
array.SetValue("Bill",2);
for(int i = 0;i < array.Length;i++)
{
  Console.WriteLine(array.GetValue(i));
}

ArrayList

ArrayList 类是一个特殊的数组,他所属的名称空间是 System.Collections。

优点

  • · 支持自动改变大小的功能
  • · 可以灵活的插入元素
  • · 可以灵活的删除元素
  • · 可以灵活的访问元素

缺点,跟一般的数组相比,速度上差一些

ArrayList myArrayList = new ArrayList();
myArrayList.Add(1);
myArrayList.Add(2);
myArrayList.Add(3);

//或者可以直接添加一个数组给 ArrayList
nt[] myInt = {1,2,3};
myArrayList.AddRange(myInt);

添加元素

//将对象添加到ArrayList的结尾处
public virtual int Add(object value);

//将元素插入ArrayList的指定索引处
public virtual void Insert(int index,object value)

//将集合中的所有元素插入 ArrayList 的指定索引处
public virtual void InsertRange(int index,ICollectionc)

示例

ArrayList aList=new ArrayList();
aList.Add("a");
aList.Add("b");
aList.Add("c");
aList.Add("d");
aList.Add("e");    //abcde
aList.Insert(0,"aa");    //结果为 aaabcde

ArrayList list2=new ArrayList();
list2.Add("tt");
aList.InsertRange(2,list2);    //aattabcde

删除

定义

//从 ArrayList 中移除特定对象的第一个匹配项,注意是第一个
public virtual void Remove(object obj);

//移除 ArrayList 的指定索引处的元素
public virtual void RemoveAt(intindex);

//从 ArrayList 中移除一定范围的元素。Index 表示索引,count 表示从索引处开始的数目
public virtual void RemoveRange(int index,int count);

//从 ArrayList 中移除所有元素
public virtual void Clear();

示例

ArrayList aList=new ArrayList();
aList.Add("a");
aList.Add("b");
aList.Add("c");
aList.Add("d");
aList.Add("e");
aList.Remove("a");    //结果为 bcde

aList.RemoveAt(0);    //结果为 cde

aList.RemoveRange(1,2);    //c

排序

//对ArrayList或它的一部分中的元素进行排序
public virtual void Sort()

//将 ArrayList 或它的一部分中元素的顺序反转
public virtual void Reverse();

示例

ArrayListaList=newArrayList();
aList.Add("e");
aList.Add("a");
aList.Add("b");
aList.Add("c");
aList.Add("d");    //结果为 eabcd
aList.Sort();    //abcde
aList.Reverse();    //反转,edcba

用ArrayList对哈希表进行排序

对哈希表进行排序在这里的定义是对key/value键值对中的key按一定规则重新排列,但是实际上这个定义是不能实现的,因为我们无法直接在Hashtable进行对key进行重新排列,如果需要Hashtable提供某种规则的输出,可以采用一种变通的做法

实际并没有重排哈希,只是输出时达到重排的效果。就是把键取出来,对键排序而已

Hashtable hashtable = new Hashtable();
hashtable.Add("id", "BH0001");   
hashtable.Add("name", "TM");    
hashtable.Add("sex", "男");

ArrayList akeys = new ArrayList(hashtable.Keys); 
//别忘了导入System.Collections
akeys.Sort(); //按字母顺序进行排序
foreach (string skey in akeys){
    Console.WriteLine(hashtable[skey]);//排序后输出
}

查找

//返回ArrayList或它的一部分中某个值的第一个匹配项的从零开始的索引。没找到返回-1
public virtual int IndexOf(object);
public virtual int IndexOf(object,int);
public virtual int IndexOf(object,int,int);

//返回ArrayList或它的一部分中某个值的最后一个匹配项的从零开始的索引
public virtual int LastIndexOf(object);
public virtual int LastIndexOf(object,int);
public virtual int LastIndexOf(object,int,int);

//确定某个元素是否在 ArrayList 中。包含返回 true,否则返回 false
public virtual bool Contains(object item);

示例

ArrayList aList=new ArrayList();
aList.Add("a");
aList.Add("b");
aList.Add("c");
aList.Add("d");
aList.Add("e");
aList.Add("e");
int nIndex=aList.IndexOf(“a”);    //1
nIndex=aList.IndexOf(“p”);    //没找到,-1
nIndex=aList.LastIndexOf("e");    //值为4而不是5

获取数组中的元素

for(int i=0;i<10;i++){
    aList[i]...
}

其他

Capacity、Count

Capacity是ArrayList可以存储的元素数。Count是ArrayList中实际包含的元素数。Capacity总是大于或等于Count。如果在添加元素时,Count超过Capacity,则该列表的容量会通过自动重新分配内部数组加倍。

如果Capacity的值显式设置,则内部数组也需要重新分配以容纳指定的容量。如果Capacity被显式设置为0,则公共语言运行库将其设置为默认容量。默认容量为16。

在调用Clear后,Count为0,而此时Capacity切是默认容量16,而不是0。

TrimToSize

如果不向列表中添加新元素,则此方法可用于最小化列表的内存系统开销

若要完全清除列表中的所有元素,请在调用TrimToSize之前调用Clear方法。截去空ArrayList会将ArrayList的容量设置为默认容量,而不是零

//获取或设置ArrayList可包含的元素数
public virtual int Capacity{get;set;}

//获取ArrayList中实际包含的元素数
public virtual int Count{get;}

//将容量设置为ArrayList中元素的实际数量
public virtual void TrimToSize();

示例

ArrayList aList=new ArrayList();
aList.Add("a");
aList.Add("b");
aList.Add("c");
aList.Add("d");
aList.Add("e");    //Count=5,Capacity=16,
aList.TrimToSize();    //Count=Capacity=5;

数组、ArrayList和List的区别

数组

在内存中是连续存储的,所以它的索引速度非常快,而且赋值与修改元素也很简单

string[] str = new string[2];  

但是数组存在一些不足的地方。在数组的两个数据间插入数据是很麻烦的,而且在声明数组的时候必须指定数组的长度,数组的长度过长,会造成内存浪费,过段会造成数据溢出的错误。如果在声明数组时我们不清楚数组的长度,就会变得很麻烦。针对数组的这些缺点,C#中最先提供了ArrayList对象来克服这些缺点

ArrayList是命名空间System.Collections下的一部分,在使用该类时必须进行引用,同时继承了IList接口,提供了数据存储和检索。ArrayList对象的大小是按照其中存储的数据来动态扩充与收缩的。所以,在声明ArrayList对象时并不需要指定它的长度

ArrayList list = new ArrayList();

ArrayList会把所有插入其中的数据当作为object类型来处理,在我们使用ArrayList处理数据时,很可能会报类型不匹配的错误,也就是ArrayList不是类型安全的。在存储或检索值类型时通常发生装箱和取消装箱操作,带来很大的性能耗损。

因为ArrayList存在不安全类型与装箱拆箱的缺点,所以出现了泛型的概念。List类是ArrayList类的泛型等效类,它的大部分用法都与ArrayList相似,因为List类也继承了IList接口。最关键的区别在于,在声明List集合时,我们同时需要为其声明List集合内数据的对象类型。

List<string> list = new List<string>();

总结

数组的容量是固定的,您只能一次获取或设置一个元素的值,而ArrayList或List的容量可根据需要自动扩充、修改、删除或插入数据

数组可以具有多个维度,而 ArrayList或 List< T> 始终只具有一个维度。但是,您可以轻松创建数组列表或列表的列表。特定类型(Object 除外)的数组 的性能优于 ArrayList的性能。这是因为 ArrayList的元素属于 Object 类型;所以在存储或检索值类型时通常发生装箱和取消装箱操作。不过,在不需要重新分配时(即最初的容量十分接近列表的最大容量),List< T> 的性能与同类型的数组十分相近。

在决定使用 List 还是使用ArrayList 类(两者具有类似的功能)时,记住List 类在大多数情况下执行得更好并且是类型安全的。如果对List< T> 类的类型T使用引用类型,则两个类的行为是完全相同的。但是,如果对类型T使用值类型,则需要考虑实现和装箱问题。

装箱与拆箱的概念

装箱与拆箱的过程是很损耗性能的。

装箱:就是将值类型的数据打包到引用类型的实例中

拆箱:就是从引用数据中提取值类型

//装箱:将int类型的值abc赋给object对象obj
int i=123
object obj=(object)i;  

//拆箱:将object对象obj的值赋给int类型的变量i
object obj=”abc”;  
int i=(string)obj;

HashTable、Dictionary、ConcurrentDictionary

HashTable

HashTable表示键/值对的集合。在.NET Framework中,Hashtable是System.Collections命名空间提供的一个容器,用于处理和表现类似key-value的键值对,其中key通常可用来快速查找,同时key是区分大小写;value用于存储对应于key的值。Hashtable中key-value键值对均为object类型,所以Hashtable可以支持任何类型的keyvalue键值对,任何非 null 对象都可以用作键或值。

HashTable是一种散列表,他内部维护很多对Key-Value键值对,其还有一个类似索引的值叫做散列值(HashCode),它是根据GetHashCode方法对Key通过一定算法获取得到的,所有的查找操作定位操作都是基于散列值来实现找到对应的Key和Value值的

散列函数(GetHashCode)让散列值对应HashTable的空间地址尽量不重复。

当一个HashTable被占用一大半的时候我们通过计算散列值取得的地址值可能会重复指向同一地址,这就造成哈希冲突。

C#中键值对在HashTable中的位置Position= (HashCode& 0x7FFFFFFF) % HashTable.Length,C#是通过探测法解决哈希冲突的,当通过散列值取得的位置Postion以及被占用的时候,就会增加一个位移x值判断下一个位置Postion+x是否被占用,如果仍然被占用就继续往下位移x判断Position+2*x位置是否被占用,如果没有被占用则将值放入其中。当HashTable中的可用空间越来越小时,则获取得到可用空间的难度越来越大,消耗的时间就越多。

使用方法如下:

using System;
using System.Collections;
namespace WebApp;

class Program {
    static void Main(string[] args) {
        Hashtable myHash = new Hashtable();
        //插入
        myHash.Add("1", "joye.net");
        myHash.Add("2", "joye.net2");
        myHash.Add("3", "joye.net3");
        //key 存在
        try { myHash.Add("1", "1joye.net"); }
        catch { Console.WriteLine("Key = "1" already exists."); }
        //取值
        Console.WriteLine("key = "2", value = {0}.", myHash["2"]);
        //修改
        myHash["2"] = "http://www.cnblogs.com/yinrq/";
        myHash["4"] = "joye.net4";
        //修改的key不存在则新增
        Console.WriteLine("key = "2", value = {0}.", myHash["2"]);
        Console.WriteLine("key = "4", value = {0}.", myHash["4"]);
        //判断key是否存在
        if (!myHash.ContainsKey("5")) {
            myHash.Add("5", "joye.net5");
            Console.WriteLine("key = "5": {0}", myHash["5"]);
        }
        //移除
        myHash.Remove("1");
        if (!myHash.ContainsKey("1")) { Console.WriteLine("Key "1" is not found."); }
        //foreach 取值
        foreach (DictionaryEntry item in myHash) { Console.WriteLine("Key = {0}, Value = {1}", item.Key, item.Value); }
        //所有的值
        foreach (var item in myHash.Values) { Console.WriteLine("Value = {0}", item); }
        //所有的key
        foreach (var item in myHash.Keys) { Console.WriteLine("Key = {0}", item); }
        Console.ReadKey();
    }
}

Dictionary

Dictionary<TKey, TValue> 泛型类提供了从一组键到一组值的映射。通过键来检索值的速度是非常快的,接近于 O(1),这是因为 Dictionary<TKey, TValue> 类是作为一个哈希表来实现的。检索速度取决于为 TKey 指定的类型的哈希算法的质量。TValue可以是值类型,数组,类或其他。

Dictionary是一种变种的HashTable,它采用一种分离链接散列表的数据结构来解决哈希冲突的问题。

class Program {
    static void Main(string[] args) {
        Dictionary<string, string> myDic = new Dictionary<string, string>();
        //插入
        myDic.Add("1", "joye.net");
        myDic.Add("2", "joye.net2");
        myDic.Add("3", "joye.net3");
        //key 存在
        try { myDic.Add("1", "1joye.net"); }
        catch {
            Console.WriteLine("Key = "1" already exists.");
        }
        //取值
        Console.WriteLine("key = "2", value = {0}.", myDic["2"]);
        //修改
        myDic["2"] = "http://www.cnblogs.com/yinrq/"; 
        myDic["4"] = "joye.net4";
        //修改的key不存在则新增
        Console.WriteLine("key = "2", value = {0}.", myDic["2"]);
        Console.WriteLine("key = "4", value = {0}.", myDic["4"]);
        //判断key是否存在
        if (!myDic.ContainsKey("5")) {
            myDic.Add("5", "joye.net5");
            Console.WriteLine("key = "5": {0}", myDic["5"]);
        }
        //移除
        myDic.Remove("1");
        if (!myDic.ContainsKey("1")) { Console.WriteLine("Key "1" is not found."); }
        //foreach 取值
        foreach (var item in myDic) { Console.WriteLine("Key = {0}, Value = {1}", item.Key, item.Value); }
        //所有的值
        foreach (var item in myDic.Values) { Console.WriteLine("Value = {0}", item); }
        //所有的key
        foreach (var item in myDic.Keys) { Console.WriteLine("Key = {0}", item); }
        Console.ReadKey();
    }
}

ConcurrentDictionary

表示可由多个线程同时访问的键/值对的线程安全集合

ConcurrentDictionary<TKey, TValue> framework4出现的,可由多个线程同时访问,且线程安全。用法同Dictionary很多相同,但是多了一些方法。ConcurrentDictionary 属于System.Collections.Concurrent 命名空间按照MSDN上所说:

System.Collections.Concurrent 命名空间提供多个线程安全集合类。当有多个线程并发访问集合时,应使用这些类代替 System.Collections 和 System.Collections.Generic 命名空间中的对应类型。

对比总结

大数据插入:Dictionary花费时间最少

遍历:HashTable最快,是Dictionary的1/5,ConcurrentDictionary的1/10

单线程建议用Dictionary,多线程建议用ConcurrentDictionary或者HashTable(Hashtable tab = Hashtable.Synchronized(new Hashtable());获得线程安全的对象)

Hashtable和Dictionary区别

推荐使用Dictionary

1、Dictionary<K,V>在使用中是顺序存储的,而Hashtable由于使用的是哈希算法进行数据存储,是无序的。

2、Dictionary的key和value是泛型存储,Hashtable的key和value都是object

3、Dictionary是泛型存储,不需要进行类型转换,Hashtable由于使用object,在存储或者读取值时都需要进行类型转换,所以比较耗时

4、单线程程序中推荐使用 Dictionary, 有泛型优势, 且读取速度较快, 容量利用更充分。多线程程序中推荐使用 Hashtable, 默认的 Hashtable 允许单线程写入, 多线程读取, 对 Hashtable 进一步调用 Synchronized() 方法可以获得完全线程安全的类型. 而 Dictionary 非线程安全, 必须人为使用 lock 语句进行保护, 效率大减。

5、在通过代码测试的时候发现key是整数型Dictionary的效率比Hashtable快,如果key是字符串型,Dictionary的效率没有Hashtable快。

对于如何进行选择,个人倾向于使用Dictionary,原因是:

1、Dictionary是可排序的,Hashtable如果想排序还需要采用别的方式进行

2、Dictionary有泛型优势,效率要高

List

API

属性

名称说明
Count
Capacity容纳元素的数量,不是count

实例方法

名称说明示例补充
Add添加单个元素
AddRange添加序列
Insert、InsertRange在指定位置插入元素或序列
Clear移除所有元素
Remove删除第一个匹配到的元素
RemoveAll删除满足条件的所有元素,返回删除的元素个数RemoveAll(i => i > 3)
RemoveAt根据下标删除元素
RemoveRange删除指定位置和数量的元素
Exists是否存在满足指定条件的元素Exists(i => i > 3)
Find、FindLast查找满足条件的第一个或最后一个元素Find(i => i > 3)
FindAll查找满足条件的所有元素,返回listFindAll(i => i > 3)
FindIndex、FindLastIndex查找满足条件的第一个或最后一个元素下标,找不到返回-1FindIndex(i => i > 3)
IndexOf、LastIndexOf查找元素,返回第一个或找到的下标,找不到返回-1。可以在指定范围内查找复杂度logn,必须是排序后的list
BinarySearch二分查找,返回元素首次出现的下标,找不到返回负数(负几结果不好说)复杂度logn,必须是排序后的list
Foreach对每个元素执行指定操作,参数是Action委托
TrueForAll判断所有元素是否都满足某条件TrueForAll(i => i > 0)
Reverse
ToArray
Contains
Sort默认从小到大排序
TrimExcess将容量设置为元素的实际个数

示例

BinarySearch(0,5,4, new test())    //在从0开始的5个元素范围内,使用自定义比较器查找元素4
BinarySearch(4, new test())    //使用自定义比较器查找
BinarySearch(4)

定义一个二维List

C# 中没有内置带两列的 List 结构,但还是有其他解决方案的,大体上有四种

方案一:使用 Tuple<int, string>

static void Main(string[] args)
{
    //C#7 之前的版本使用Tuple<int, string>
    List<Tuple<intstring>> mylist = new List<Tuple<intstring>>();

    // add an item
    mylist.Add(new Tuple<intstring>(someInt, someString));
}

使用ValueTuple

static void Main(string[] args)
{
    // C#7 以后的版本,可以使用新的结构 ValueTuple
    List<(intstring)> mylist = new List<(intstring)>();
}

值得一提的是,在 .NETFramework 4.7+ 和 .NET Core 中是内置的,它是引用类型 Tuple 的值类型版本,也比 Tuple 更加灵活,比如下面这样

static void Main(string[] args)
{
    var mylist = new List<(int myInt, string myString)>();
}

方案二:使用 Dictionary<int,string>

方案三:使用 struct

可以将 key-value 封装到 struct 结构体中,这样更加可视化,参考如下代码:

public struct Data
{
    public Data(int intValue, string strValue)
    {
        IntegerData = intValue;
        StringData = strValue;
    }
    public int IntegerData { getprivate set; }
    public string StringData { getprivate set; }
}

class Program
{
    static void Main(string[] args)
    {
        var list = new List<Data>();
    }
}

将匿名类型添加到 List

最优方案,使用valueTurple类型

var o = new { Id = 1, Name = "Foo" };
var o1 = new { Id = 2, Name = "Bar" };
//伪代码如下
List<var> list = new List<var>();
list.Add(o);
list.Add(o1);

方式一:初始化出带有匿名的 List

var o = new { Id = 1, Name = "Foo" };
var o1 = new { Id = 2, Name = "Bar" };

var array = new[] { o, o1 };
//将数组转换为List
var list = array.ToList();
list.Add(new { Id = 3, Name = "Yeah" });

方式二:用ValueTuple

如果你使用的是 C#7 以上,建议用 ValueTuple 来替代匿名类型

static void Main(string[] args)
{
    var list = new List<(int Id, string Name)>();
    list.Add((Id1, Name"Foo"));
    list.Add((Id2, Name"Bar"));
}

方式三:使用dynamic类型的 List

List<dynamic> anons=new List<dynamic>();
foreach (Model model in models)
{
   var anon= new
   {
      Id = model.Id,
      Name=model.Name
   };
   anons.Add(anon);
}

对List中的元素洗牌

建议写一个扩展方法,使用new Random的方式

方式一:Guid排序

GUID并不能保证完全随机化

var shuffledcards = cards.OrderBy(a => Guid.NewGuid()).ToList();

方式二:使用 Random 类替代

private static Random rng = new Random();
var shuffledcards = cards.OrderBy(a => rng.Next()).ToList();

方式三:使用两个List

将一个List中的元素随机插入到另一个List的某位置

List<int> xList = new List<int>() { 12345 };
List<int> deck = new List<int>();
//可能会插重复的位置把
foreach (int xInt in xList)
    deck.Insert(random.Next(0, deck.Count + 1), xInt);

方式四:使用扩展方法

public static class IEnumerableExtensions
{
    public static IEnumerable<tRandomize<t>(this IEnumerable<t> target)
    {
        Random r = new Random();
        return target.OrderBy(x=>(r.Next()));
    }        
}
// use this on any collection that implements IEnumerable!
// List, Array, HashSet, Collection, etc
//调用
List<string> myList = new List<string> { "hello""random""world""foo""bar""bat""baz" };
foreach (string s in myList.Randomize())
{
    Console.WriteLine(s);
}

如何理解List、Dictionary的扩容机制

为什么 List 是按照 2倍 扩容

private void EnsureCapacity(int min)
{
    if (this._items.Length < min)
    {
        int num = (this._items.Length == 0) ? 4 : (this._items.Length * 2);
        if (num < min)
        {
            num = min;
        }
        this.Capacity = num;
    }
}

而 Dictionary<K,V> 是按 素数 扩容

private void Resize()
{
    int prime = HashHelpers.GetPrime(this.count * 2);
    int[] numArray = new int[prime];
    for (int i = 0; i < numArray.Length; i++)
    {
        numArray[i] = -1;
    }
    Entry<TKey, TValue>[] destinationArray = new Entry<TKey, TValue>[prime];
    Array.Copy(this.entries, 0, destinationArray, 0, this.count);
    for (int j = 0; j < this.count; j++)
    {
        int index = destinationArray[j].hashCode % prime;
        destinationArray[j].next = numArray[index];
        numArray[index] = j;
    }
    this.buckets = numArray;
    this.entries = destinationArray;
}

为什么不都按照 2倍 来呢?这样还可以实现代码复用

Dictionary 是启发式的,它需要保证 hashcode 必须准均匀的分布在各个桶中,.NET 的 Dictionary 采用的是素数来实现这个均衡。List 不是启发式的,所以没有这么烦恼

Dictionary采用素数,本质目的就是减少桶挂链,减少hashcode冲突,如果挂链过长,那就达不到 Contains,Add,Get 等操作的 O(1) 时间复杂度,这也就失去了 Dictionary 原本的特性。

下面是计算桶索引的算法

int num = this.comparer.GetHashCode(key) & 2147483647; // make hash code positive
// get the remainder from division - that's our bucket index
int num2 = this.buckets[num % ((int)this.buckets.Length)];

不过 Java 采用的和 .NET 中的 List 是一样的扩容机制。

resize(2 * table.length);

在翻倍之后,java 会重新计算 hashcode 值的

static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
    return h & (length-1);
}

// from put() method
int hash = hash(key.hashCode()); // get modified hash
int i = indexFor(hash, table.length); // trim the hash to the bucket count

自定义比较器List去重

List中的项如下

class Items
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Code { get; set; }
    public decimal Price { get; set; }
}

如何通过 linq 的方式剔除重复记录?

方式一:自定义比较器

需要给 Items 一个自定义比较器

class DistinctItemComparer : IEqualityComparer<Item> {

    public bool Equals(Item x, Item y) {
        return x.Id == y.Id &&
            x.Name == y.Name &&
            x.Code == y.Code &&
            x.Price == y.Price;
    }

    public int GetHashCode(Item obj) {
        return obj.Id.GetHashCode() ^
            obj.Name.GetHashCode() ^
            obj.Code.GetHashCode() ^
            obj.Price.GetHashCode();
    }
}
//有了这个比较器,后面就方便了
var distinctItems = items.Distinct(new DistinctItemComparer());

方式二:GroupBy + Anonymous

使用 GroupBy,把 Items 的所有属性名灌到匿名类型上

var distinctItems = a.GroupBy(c => new { c.Id , c.Name , c.Code , c.Price})
                     .Select(c => c.First()).ToList();

方式三:重写 Equals 和 GetHashCode 方法

public class Item
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Code { get; set; }
    public int Price { get; set; }

    public override bool Equals(object obj)
    {
        if (!(obj is Item))
            return false;
        Item p = (Item)obj;
        return (p.Id == Id && p.Name == Name && p.Code == Code && p.Price == Price);
    }
    public override int GetHashCode()
    {
        return String.Format("{0}|{1}|{2}|{3}", Id, Name, Code, Price).GetHashCode();
    }
}
//然后就可以直接通过 Distinct() 过滤啦
var distinctItems = a.Distinct();

排序

方法一:调用sort方法,如果需要降序,进行反转

List<int> list = new List<int>();
list.Sort();// 升序排序
list.Reverse();// 反转顺序

方法二:使用lambda表达式,在前面加个负号就是降序了

List<int> list= new List<int>(){5,1,22,11,4};
list.Sort((x, y) => x.CompareTo(y));//升序
list.Sort((x, y) => -x.CompareTo(y));//降序

Sort((i,j)=>i-j)  //从小到大
Sort((i,j)=>j-i) //从大到小

Dictionary

说明

  • 必须包含名空间System.Collection.Generic
  • Dictionary里面的每一个元素都是一个键值对(由二个元素组成:键和值)
  • 键必须是唯一的,而值不需要唯一的
  • 键和值都可以是任何类型(比如:string, int, 自定义类型,等等)
  • 通过一个键读取一个值的时间是接近O(1)
  • 键值对之间的偏序可以不定义

API

常用属性

  • Comparer 获取用于确定字典中的键是否相等的 IEqualityComparer。
  • Count 获取包含在 Dictionary<TKey, TValue> 中的键/值对的数目。
  • Item 获取或设置与指定的键相关联的值。
  • Keys 获取包含 Dictionary<TKey, TValue> 中的键的集合。
  • Values 获取包含 Dictionary<TKey, TValue> 中的值的集合。

常用方法

  • Add 将指定的键和值添加到字典中。
  • Clear 从 Dictionary<TKey, TValue> 中移除所有的键和值。
  • ContainsKey 确定 Dictionary<TKey, TValue> 是否包含指定的键。
  • ContainsValue 确定 Dictionary<TKey, TValue> 是否包含特定值。
  • Equals(Object) 确定指定的 Object 是否等于当前的 Object。(继承自 Object。)
  • Finalize 允许对象在“垃圾回收”回收之前尝试释放资源并执行其他清理操作。(继承自 Object。)
  • GetEnumerator 返回循环访问 Dictionary<TKey, TValue> 的枚举器。
  • GetHashCode 用作特定类型的哈希函数。(继承自 Object。)
  • GetObjectData 实现 System.Runtime.Serialization.ISerializable 接口,并返回序列化 Dictionary<TKey, TValue> 实例所需的数据。
  • GetType 获取当前实例的 Type。(继承自 Object。)
  • MemberwiseClone 创建当前 Object 的浅表副本。(继承自 Object。)
  • OnDeserialization 实现 System.Runtime.Serialization.ISerializable 接口,并在完成反序列化之后引发反序列化事件。
  • Remove 从 Dictionary<TKey, TValue> 中移除所指定的键的值。
  • ToString 返回表示当前对象的字符串。(继承自 Object。)
  • TryGetValue 获取与指定的键相关联的值。
//定义
Dictionary<string, string> openWith = new Dictionary<string, string>();


//添加元素
openWith.Add("txt", "notepad.exe");
openWith.Add("bmp", "paint.exe");
openWith.Add("dib", "paint.exe");
openWith.Add("rtf", "wordpad.exe");

//取值
Console.WriteLine("For key = "rtf", value = {0}.", openWith["rtf"]);


//更改值
openWith["rtf"] = "winword.exe";


//遍历key
foreach (string key in openWith.Keys)
{
    Console.WriteLine("Key = {0}", key);
}


//遍历value
foreach (string value in openWith.Values)
{
    Console.WriteLine("value = {0}", value);
}

//遍历value, Second Method
Dictionary<string, string>.ValueCollection valueColl = openWith.Values;
foreach (string s in valueColl)
{
    Console.WriteLine("Second Method, Value = {0}", s);
}

//遍历字典
foreach (KeyValuePair<string, string> kvp in openWith)
{
    Console.WriteLine("Key = {0}, Value = {1}", kvp.Key, kvp.Value);
}


//删除元素
openWith.Remove("doc");


//判断键存在
if (openWith.ContainsKey("bmp")) // True 
{
    Console.WriteLine("An element with Key = "bmp" exists.");
}

增加AddRange扩展方法

最好通过枚举的方式指定下遇到重复key的情况该执行什么路径

需要考虑当遇到重复key的时候该如何处理

namespace MyProject.Helper
{
  public static class CollectionHelper
  {
    //键重复则覆盖
    public static void AddRangeOverride<TKey, TValue>(this IDictionary<TKey, TValue> dic, IDictionary<TKey, TValue> dicToAdd)
    {
        dicToAdd.ForEach(x => dic[x.Key] = x.Value);
    }

    //键重复,添加后面的

    public static void AddRangeNewOnly<TKey, TValue>(this IDictionary<TKey, TValue> dic, IDictionary<TKey, TValue> dicToAdd)
    {
        dicToAdd.ForEach(x => { if (!dic.ContainsKey(x.Key)) dic.Add(x.Key, x.Value); });
    }

    //键重复,直接报错抛异常

    public static void AddRange<TKey, TValue>(this IDictionary<TKey, TValue> dic, IDictionary<TKey, TValue> dicToAdd)
    {
        dicToAdd.ForEach(x => dic.Add(x.Key, x.Value));
    }

    //判断是否包含keys

    public static bool ContainsKeys<TKey, TValue>(this IDictionary<TKey, TValue> dic, IEnumerable<TKey> keys)
    {
        bool result = false;
        keys.ForEachOrBreak((x) => { result = dic.ContainsKey(x); return result; });
        return result;
    }

    //对每个元素执行操作

    public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
    {
        foreach (var item in source)
            action(item);
    }

    //满足条件执行操作,否则退出

    public static void ForEachOrBreak<T>(this IEnumerable<T> source, Func<T, bool> func)
    {
        foreach (var item in source)
        {
            bool result = func(item);
            if (result) break;
        }
    }
  }
}

然后像下面这样使用

Dictionary<string, string> mainDic = new Dictionary<string, string>() { 
    { "Key1", "Value1" },
    { "Key2", "Value2.1" },
};
Dictionary<string, string> additionalDic= new Dictionary<string, string>() { 
    { "Key2", "Value2.2" },
    { "Key3", "Value3" },
};
mainDic.AddRangeOverride(additionalDic); // Overrides all existing keys
// or
mainDic.AddRangeNewOnly(additionalDic); // Adds new keys only
// or
mainDic.AddRange(additionalDic); // Throws an error if keys already exist
// or
if (!mainDic.ContainsKeys(additionalDic.Keys)) // Checks if keys don't exist
{
    mainDic.AddRange(additionalDic);
}

Dictionary.Clear

Dictionary.Clear 和 new Dictionary() 有什么不同?

编程中的区别:1)new Dictionary()会改变原this的指向 2)new 操作容易在 heap 上产生太多的垃圾数据

Dictionary.Clear() 方法会移除 Dictionary 中的所有 keyvalue 项,对应到内部就是 _bukects 和 _entries 数组,可以从源码中看的最清楚

public void Clear()
{
    int count = _count;
    if (count > 0)
    {
        Array.Clear(_buckets, 0, _buckets.Length);
        _count = 0;
        _freeList = -1;
        _freeCount = 0;
        Array.Clear(_entries, 0, count);
    }
}

而 new Dictionary() 会在托管堆上创建一个 dictionary 的一个实例。编程实践中,他们大多可以实现相同的效果,主要取决你是否需要保留原 Dictionary,也就是 this 。

void NewDictionary(Dictionary<string,int> dict)
{
   dict = new Dictionary<string,int>(); // Just changes the local reference
}
void  ClearDictionary(Dictionary<string,int> dict)
{
   dict.Clear();
}

// When you use this...
Dictionary<string,int> myDictionary = ...; // Set up and fill dictionary

NewDictionary(myDictionary);
// myDictionary is unchanged here, since we made a new copy, but didn't change the original instance

ClearDictionary(myDictionary);
// myDictionary is now empty

对Dictionary的value排序

举个例子:我有一个 word 和相应单词 频次 的hash对,现在我想按照 频次 对 word 进行排序。

若使用 SortList 实现,只能实现单值排序,比如存放频次,但这样我还要通过它反找 word,貌似不好实现,在 .NET 框架中还有一个 SortDictionary ,但是它只能按照 key 排序,要想硬实现还得定义一些自定义类。

方法一:Linq

用 Linq 可以给我们带来非常大的灵活性,它可以获取 top10, top20,还有 top10% 等等

Dictionary<string, int> myDict = new Dictionary<string, int>();
myDict.Add("one", 1);
myDict.Add("four", 4);
myDict.Add("two", 2);
myDict.Add("three", 3);

var sortedDict = from entry in myDict orderby entry.Value ascending select entry;

//或者
var ordered = dict.OrderBy(x => x.Value).ToDictionary(x => x.Key, x => x.Value);

方法二:转成 List 然后使用自带的 Sort 方法进行排序

Dictionary<string, string> s = new Dictionary<string, string>();
s.Add("1", "a Item");
s.Add("2", "c Item");
s.Add("3", "b Item");

List<KeyValuePair<string, string>> myList = new List<KeyValuePair<string, string>>(s);
myList.Sort(
    delegate(KeyValuePair<string, string> firstPair,
    KeyValuePair<string, string> nextPair)
    {
        return firstPair.Value.CompareTo(nextPair.Value);
    }
);

实现一个带值变更通知能力的Dictionary

DictionaryWapper的表征实现也得益于C#索引器特性

using System;
using System.Collections.Generic;
using System.Text;
namespace DAL
{
    //变更时传递的事件参数ValueChangedEventArgs<TKey>
    public class ValueChangedEventArgs<TK> : EventArgs
    {
        public TK Key { get; set; }
        public ValueChangedEventArgs(TK key)
        {
            Key = key;
        }
    }

    public class DictionaryWapper<TKey, TValue>
    {
        public object  objLock = new object();
       
        private Dictionary<TKey, TValue> _dict;
        //定义值变更事件OnValueChanged
        public event EventHandler<ValueChangedEventArgs<TKey>> OnValueChanged;
        public DictionaryWapper(Dictionary<TKey, TValue> dict)
        {
            _dict = dict;
        }
        public TValue this[TKey Key]
        {
            get { return _dict[Key]; }
            set
            {
                lock(objLock)
                {
                    try
                    {
                        //如何定义值变更
                        if (_dict.ContainsKey(Key) && _dict[Key] != null && !_dict[Key].Equals(value))
                        {
                            OnValueChanged(this, new ValueChangedEventArgs<TKey>(Key));
                        }
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"检测值变更或者触发值变更事件,发生未知异常{ex}");
                    }
                    finally
                    {
                        _dict[Key] = value;
                    }
                }
            }
        }
    }
}

订阅值变更事件

var _dictionaryWapper = new DictionaryWapper<string, string>(new Dictionary<string, string> { });
_dictionaryWapper.OnValueChanged += new EventHandler<ValueChangedEventArgs<string>>(OnConfigUsedChanged);
//----
public static void OnConfigUsedChanged(object sender, ValueChangedEventArgs<string> e)
{
   Console.WriteLine($"字典KEY:{e.Key}的值发生了变更,请注意...");          
}

//最后像正常Dictionary一样为DictionaryWapper添加键值对
dictionaryWapper[$"{dbConfig}:{connectionConfig.Provider}"] = connection.ConnectionString;

字符串

方法

方法名说明
split默认以空格分割字符串