面试 | .NET基础知识快速通关!
共 5365字,需浏览 11分钟
·
2021-05-11 16:33
此系列文章为我在2015年发布于博客园的.NET基础拾遗系列,它十分适合初中级.NET开发工程师在面试前进行一个系统的复习,因此我将其搬到公众号分享与你。
本文为第六篇,我们会对.NET的集合与泛型相关考点进行基础复习,全文会以Q/A的形式展现,即以面试题的形式来描述。
在.NET中的数组类型和C++中区别很大,.NET中无论是存储值类型对象的数组还是存储引用类型的数组,其本身都是引用类型,其内存也都是分配在堆上的。它们的共同特征在于:所有的数组类型都继承自System.Array,而System.Array又实现了多个接口,并且直接继承自System.Object。不同之处则在于存储值类型对象的数组所有的值都已经包含在数组内,而存储引用类型对象的数组,其值则是一个引用,指向位于托管堆中的实例对象。
下图直观地展示了二者内存分配的差别(假设object[]中存储都是DateTime类型的对象实例):
在.NET中CLR会检测所有对数组的访问,任何视图访问数组边界以外的代码都会产生一个IndexOutOfRangeException异常。
数组类型的转换需要遵循以下两个原则:
(1)包含值类型的数组不能被隐式转换成其他任何类型;
(2)两个数组类型能够相互转换的一个前提是两者维数相同;
我们可以通过以下代码来看看数组类型转换的机制:
// 编译成功
string[] sz = { "a", "a", "a" };
object[] oz = sz;
// 编译失败,值类型的数组不能被转换
int[] sz2 = { 1, 2, 3 };
object[] oz2 = sz;
// 编译失败,两者维数不同
string[,] sz3 = { { "a", "b" }, { "a", "c" } };
object[] oz3 = sz3;
除了类型上的转换,我们平时还可能会遇到内容转换的需求。例如,在一系列的用户界面操作之后,系统的后台可能会得到一个DateTime的数组,而现在的任务则是将它们存储到数据库中,而数据库访问层提供的接口只接受String[]参数,这时我们要做的就是把DateTime[]从内容上转换为String[]对象。当然,惯常做法是遍历整个源数组,逐一地转换每个对象并且将其放入一个目标数组类型容器中,最后再生成目标数组。But,这里我们推荐使用Array.ConvertAll方法,它提供了一个简便的转换数组间内容的接口,我们只需指定源数组的类型、对象数组的类型和具体的转换算法,该方法就能高效地完成转换工作。
下面的代码清楚地展示了普通的数组内容转换方式和使用Array.ConvertAll的数组内容转换方式的区别:
public class Program
{
public static void Main(string[] args)
{
String[] times ={"2008-1-1",
"2008-1-2",
"2008-1-3"};
// 使用不同的方法转换
DateTime[] result1 = OneByOne(times);
DateTime[] result2 = ConvertAll(times);
// 结果是相同的
Console.WriteLine("手动逐个转换的方法:");
foreach (DateTime item in result1)
{
Console.WriteLine(item.ToString("yyyy-MM-dd"));
}
Console.WriteLine("使用Array.Convert方法:");
foreach (DateTime item2 in result2)
{
Console.WriteLine(item2.ToString("yyyy-MM-dd"));
}
Console.ReadKey();
}
// 逐个手动转换
private static DateTime[] OneByOne(String[] times)
{
List<DateTime> result = new List<DateTime>();
foreach (String item in times)
{
result.Add(DateTime.Parse(item));
}
return result.ToArray();
}
// 使用Array.ConertAll方法
private static DateTime[] ConvertAll(String[] times)
{
return Array.ConvertAll(times,
new Converter<String, DateTime>
(DateTimeToString));
}
private static DateTime DateTimeToString(String time)
{
return DateTime.Parse(time);
}
}
从上述代码可以看出,二者实现了相同的功能,但是Array.ConvertAll不需要我们手动地遍历数组,也不需要生成一个临时的容器对象,更突出的优势是它可以接受一个动态的算法作为具体的转换逻辑。当然,明眼人一看就知道,它是以一个委托的形式作为参数传入,这样的机制保证了Array.ConvertAll具有较高的灵活性。
泛型的语法和概念类似于C++中的template(模板),它是.NET 2.0中推出的众多特性中最为重要的一个,方便我们设计更加通用的类型,也避免了容器操作中的装箱和拆箱操作。
假如我们要实现一个排序算法,要求能够针对各种类型进行排序。按照以前的做法,我们需要对int、double、float等类型都实现一次,但是我们发现除了数据类型,其他的处理逻辑完全一致。这时,我们便可以考虑使用泛型来进行实现:
public static class SortHelper<T> where T : IComparable
{
public static void BubbleSort(T[] array)
{
int length = array.Length;
for (int i = 0; i <= length - 2; i++)
{
for (int j = length - 1; j >= 1; j--)
{
// 对两个元素进行交换
if (array[j].CompareTo(array[j - 1]) < 0)
{
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
Tips:Microsoft在产品文档中建议所有的泛型参数名称都以T开头,作为一个中编码的通用规范,建议大家都能遵守这样的规范,类似的规范还有所有的接口都以I开头。
泛型类型和普通类型有一定的区别,通常泛型类型被称为开放式类型,.NET中规定开放式类型不能实例化,这样也就确保了开放式类型的泛型参数在被指定前,不会被实例化成任何对象(事实上,.NET也没有办法确定到底要分配多少内存给开放式类型)。为开放式的类型提供泛型的实例导致了一个新的封闭类型的生成,但这并不代表新的封闭类型和开放类型有任何继承关系,它们在类结构图上是处于同一层次,并且两者之间没有任何关系。下图展示了这一概念:
此外,在.NET中的System.Collections.Generic命名空间下提供了诸如List<T>、Dictionary<T>、LinkedList<T>等泛型数据结构,并且在System.Array中定义了一些静态的泛型方法,我们应该在编码实践时充分使用这些泛型容器,以提高我们的开发和系统的运行效率。
当一个泛型参数没有任何约束时,它可以进行的操作和运算是非常有限的,因为不能对实参进行任何类型上的保证,这时候就需要用到泛型约束。泛型的约束分为:主要约束和次要约束,它们都使实参必须满足一定的规范,C#编译器在编译的过程中可以根据约束来检查所有泛型类型的实参并确保其满足约束条件。
(1)主要约束
一个泛型参数至多拥有一个主要约束,主要约束可以是一个引用类型、class或者struct。如果指定一个引用类型(class),那么实参必须是该类型或者该类型的派生类型。相反,struct则规定了实参必须是一个值类型。下面的代码展示了泛型参数主要约束:
public class ClassT1<T> where T : Exception
{
private T myException;
public ClassT1(T t)
{
myException = t;
}
public override string ToString()
{
// 主要约束保证了myException拥有source成员
return myException.Source;
}
}
public class ClassT2<T> where T : class
{
private T myT;
public void Clear()
{
// T是引用类型,可以置null
myT = null;
}
}
public class ClassT3<T> where T : struct
{
private T myT;
public override string ToString()
{
// T是值类型,不会发生NullReferenceException异常
return myT.ToString();
}
}
泛型参数有了主要约束后,也就能够在类型中对其进行一定的操作了。
(2)次要约束
次要约束主要是指实参实现的接口的限定。对于一个泛型,可以有0到无限的次要约束,次要约束规定了实参必须实现所有的次要约束中规定的接口。次要约束与主要约束的语法基本一致,区别仅在于提供的不是一个引用类型而是一个或多个接口。例如我们为上面代码中的ClassT3增加一个次要约束:
public class ClassT3<T> where T : struct, IComparable
{
......
}
本文总结复习了.NET的集合与泛型处理相关的重要知识点,下一篇会总结.NET中流与序列化处理相关的重要知识点,欢迎继续关注!
谷歌灵魂插件,98%的程序员都好评!
再见Vip,免费看网飞影视大片!