C#学习笔记12:泛型方法、泛型类、List类型、大练习作业

-呈阶梯状分布- 47 次阅读 2411 字 发布于 28 天前 最后更新于 28 天前 C#学习笔记


泛型方法

我们希望写一个名为Test的函数,返回int[]类型数组的倒序重组,可以这么写:

static int[] Test(int[] array)
{
    int[] newArray = new int[array.Length];
    for (int i = 0; i < array.Length; i++)
    {
        newArray[array.Length-i-1] = array[i];
    }
    return newArray;
}

如果我们希望这个函数还可以实现double类型数组,可以对其添加一个重载:

static double[] Test(double[] array)
{
    double[] newArray = new double[array.Length];
    for (int i = 0; i < array.Length; i++)
    {
        newArray[array.Length-i-1] = array[i];
    }
    return newArray;
}

但是,若我们希望这个函数实现无数种类型数组,用重载就要写无数行代码了。
而且,若数组的类型是用户自己创建的类,我们就无法预判其类型,并给出对应的重载。
于是,我们就可以用泛型方法——在调用函数时传入参数类型,以此确定参数类型。
代码如下:

static Test[] Test<T>(T[] array)
{
    T[] newArray = new T[array.Length];
    for (int i = 0; i < array.Length; i++)
    {
        newArray[array.Length-i-1] = array[i];
    }
    return newArray;
}

可以看到,我们将int、double这些具体的类型改成了一个T,且在方法名后加上了<T>,这样就可以在实际允许时确认数组中的元素类型了,使用如下:

Test<int>(new int[] {91, 78, 13})

可以看到,调用和正常函数相同,但是方法名后加上了<int>,代表将T类型传为int,方法中出现的所有T都会被替换为int。
我们也可以同时指定多个类型:

using System;

namespace Csharp
{
    class Program
    {
        static void Test<TFirst, TSecond>()
        {
            Console.WriteLine(typeof(TFirst));
            Console.WriteLine(typeof(TSecond));
        }
        static void Main(string[] args)
        {
            Test<int, float>();
        }
    }
}

我们定义了一个没有参数,但要求传入两个类型的泛型方法,作用是输出这两个传入的类型,typeof()函数用于获取泛型背后的真实类型。
可以看到,要传入多个类型时,就像传入多个参数一样,用逗号隔开即可。
顺带一提,只有一个类型时一般写T,有多个类型时一般使用T开头的,且有实际意义的泛型名,以便理解其含义。
你可以认为泛型T就是一个形参,只不过是一个存储着类型的形参。

泛型类

泛型类与泛型方法类似,不过添加<T>的位置放到了类名后,这里简单举个例子,其余部分参照泛型方法即可:

using System;

namespace Csharp
{
    class TestClass<TFirst, TSecond>
    {
        private TFirst firstValue;
        private TSecond secondValue;
        public TestClass(TFirst firstValue, TSecond secondValue)
        {
            this.firstValue = firstValue;
            this.secondValue = secondValue;
        }
        public void PrintValues()
        {
            Console.WriteLine(firstValue);
            Console.WriteLine(secondValue);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            TestClass<int, double> testClassObject = new TestClass<int, double>(15, 9432.231);
            testClassObject.PrintValues();
        }
    }
}

可以看到,确定testClassObject的类时(即testClassObject前面的TestClass<int, double>),也需要确定泛型类型,所以你可以认为一种类+其泛型就是一种新的类,不过你也可以用var关键字就是了。

List类型

List,中文名列表,实际上其功能也就如列表一般,存储,取用,修改,添加。
你可能会疑惑,已经有一个相同功能的数组了,为什么还要出现一个列表?
数组的长度不可变,在其定义时就会确定长度,以下是我们学到的三种数组定义方式(来自C#学习笔记4):

数组类型[] 数组名称 = {元素1, 元素2, 元素3, 元素n...};
数组类型[] 数组名称 = new 数组类型[数组长度];
数组类型[] 数组名称 = new 数组类型[] {元素1, 元素2, 元素3, 元素n...};

对于第1行定义,长度由初始元素个数确定。
对于第2行定义,长度由[]内的数确定。
对于第3行定义,长度也由初始元素个数确定。
但List类型不是这样。在学习List类型的使用前,我们先了解其底层原理:

首先,List里有一个私有字段,是一个数组,传入List的元素都被放入数组。由于List是一个泛型类,所以数组的元素类型由传入的泛型确定。
你可能觉得先放进List再放进他的数组有些多此一举,但这正是List的精妙之处。
首先,List可以在定义时传入初始元素,就像数组一样。此时,数组的长度就会被设置为传入元素的个数,以此保证传入的元素都能放得下。
当你增加元素时,如果List内的数组放不下了,List就会对数组进行扩容。
假设你没在初始时传入元素,那么数组长度为0;
你添加一个元素,数组长度会被设为4,第一位放添加的元素;
添加到第5次时,由于数组放不下了,List内部会新建一个数组,长度为8,并把上个数组的4个元素复制到前4位,再在第5位放入新的元素;
以此类推,添加到第9次时,List新建长度为16的数组,复制,放入......

可以看出,List内的数组长度为2的n次方,n为从数组长度0开始的扩容次数+1。
且List就是一个数组的管理工具,你可以视为数组套了层壳,它可以帮你自动为数组扩容,还能通过方法对数组使用一些复杂操作。
接下来,正式开始学习List的语法和类方法。

List<数组类型> 变量名 = new List<数组类型>(List长度);
List<数组类型> 变量名 = new List<数组类型>(){元素1, 元素2, 元素3, 元素n...};

可以看到,List的定义和数组差不多,只不过元素的类型改为用泛型的方式定义了。
接下来是List的一些常用方法和属性,你可以参考我的笔记:

// 声明与定义
List<int> list1 = new List<int>(3);  // 3为列表的容量
List<int> list2 = new List<int>(){1, 2, 3};
// 添加 删除 修改
list2.Add(4);
list2.Remove(1);  // 按内容移除,只删除索引到的第一个数据
list2.RemoveAt(0);  // 按索引移除
list2[1] = 200;
// 插入
list2.Insert(1, 80);  // List.Insert(要插入到哪个索引的前面, 要插入的数据) 
// 访问数据
Console.WriteLine(list2[0]);
// 排序(从小到大)
list2.Sort();
// 查询数据的索引位置
Console.WriteLine(list2.IndexOf(200));  // 只返回索引到的第一个数据的下标,不存在时返回-1
Console.WriteLine(list2.LastIndexOf(200));  // 上面的是从前往后,下面的是从后往前
// 遍历数据(List.Count代表元素个数)
for (int i = 0; i < list2.Count; i++)
{
    Console.WriteLine(list2[i]);
}
foreach (int i in list2)
{
    Console.WriteLine(i);
}
// 查看与修改列表容量(容量不是元素个数!!!)
Console.WriteLine(list2.Capacity);  // 列表容量
list2.Capacity = 16;  // 修改

Add()方法,参数为要添加的元素,用于向List内添加元素,会添加在List的末尾。
Remove()方法,参数为要删除的元素,用于删除元素,会删除从左到右找到的第一个对应元素。
RemoveAt()方法,参数为要删除的元素下标,用于删除对应下标元素,删除后会把后面的元素往前移动,补上空位。
Insert()方法,参数为(插入的位置, 插入的元素),用于插入元素,你可以视为先将对应下标后方元素都向后移动,以此空出对应下标位置,然后将插入的元素放入该空位。
Sort()方法,用于从小到大排序。
IndexOf()方法,参数为要查询的数据,用于返回从左往右找到的第一个对应元素的下标。
ListIndexOf()方法,类似IndexOf()方法,只不过查找顺序变为了从右往左。

Count属性,代表List内的元素个数。
Capacity属性,代表List内的数组此时的长度。

还有,List可以像是数组一样使用索引器进行读写。

了解完这些,你就基本掌握了List,你或许可以尝试在项目中进行使用。

大练习

学了这么多,或许你也应该应用一些代码来解决问题了,所以我在这里给出一项作业,希望你能通过C#代码实现。
写下一个名为MyList的类,仿造List类,并实现以下功能:

  1. 在构造方法中传入初始元素(传入元素的数量不定)
  2. 实现索引器索引功能,且在索引超出范围时抛出异常,如throw new ArgumentOutOfRangeException("索引参数超出范围");
  3. 拥有Count和Capacity两个属性,分别代表元素个数和数组长度,其中Count只读,Capacity只能增大,变小则抛出异常,如第2条,作用域都为public
  4. 实现Add,Insert,Sort,RemoveAt,IndexOf,LastIndexOf,Remove,作用域都为public
  5. 在Add,Insert中实现扩容机制
  6. 重载==运算符,要求有两个MyList<T>类型的参数,实现当两个MyList元素数量和内容相同时返回true,否则返回false,同时重载!=运算符
  7. 拥有两个字段,一个是data,用于作为数组存元素;一个是count,作为元素个数使用,作用域都为private

注意事项如下:

  1. 扩容时注意Count和Capacity元素修改
  2. 在Add,Insert,RemoveAt,Remove中注意Count元素修改
  3. 对于无法使用==时,可以使用a.Equals(b)判断a和b是否相等
  4. 对于!=重载,取==的反义即可
  5. 建议在类内大多情况下使用属性,而非私有字段

发挥你的创造力吧!下一篇讲解大练习作业