目录
1.局部函数
从C#7.0开始,可以在一个方法中声明另一个单独的方法,这样可以将迁入的方法跟其它代码隔离开来,所以它只能在包含它的方法内调用。如果使用恰当,这可以是代码更清晰,更易维护,这些嵌入的方法被称为局部函数。
public class LocalFunction
{
public static void MethodWithLocalFunction()
{
int MyLocalFunction(int x)
{
return x * x;
}
int result = MyLocalFunction(10);
Console.WriteLine($"LocalFunction called: x*x={result}");
}
}
2.参数数组
参数数组允许特定类型的零个或者多个实参对应一个特定的形参,规则如下:
- 在一个参数列表中只能有一个参数数组
- 如果有,那必须放在最后一个
- 由参数数组表示的所有参数必须是同一类型
- 声明时,在数据类型前加params关键字
- 在数据类型后放置一组空的方括号
(数组是引用类型,所以它的所有数据都保存在堆中)
public class Params
{
public static void ListInts(string sep,params int[] vals)
{
if(vals != null && vals.Length>0)
{
for(int i = 0; i < vals.Length; i++)
{
vals[i] *= 10;
Console.Write(vals[i]+sep);
}
}
}
public static void Test()
{
int a = 5, b = 6, c = 7;
ListInts(" --- ", a, b, c);
Console.WriteLine($"\n {a} {b} {c}"); //不改变参数
}
}
3.ref局部变量和ref返回
ref有两个注意事项:
- 可以使用这个功能创建一个变量的别名,即使引用的对象是值类型
- 对任意一个变量的复制都会反应到另一个变量上,因为他们引用的是相同的对象,即使是值类型。
关于取别名,很好理解:
ref int y=ref x;
不过别名功能不是ref局部变量功能最常见的用途,它经常和ref返回功能一起使用。下面代码演示在方法调用之后,因为调用了修改ref局部变量的代码,所以类的值改变了。
public class RefUsage
{
private int score = 5;
public ref int RefToValue()
{
return ref score;
}
public void Display()
{
Console.WriteLine($"Score: {score}");
}
public static void Test()
{
RefUsage s=new RefUsage();
s.Display();
ref int x=ref s.RefToValue();
x = 10;
s.Display();
}
}
再举一个更有用的例子:一般函数库中Max函数返回的是值,而不是较大值得引用,
public static ref int Max(ref int x,ref int y)
{
if(x > y)
return ref x;
else
return ref y;
}
4.静态构造函数注意事项
通常静态构造函数初始化类的静态字段。初始化类级别的项:
在引用任何静态成员之前
在创建类的任何实例之前
静态构造函数与实例构造函数不同之处在于:
- 静态构造函数声明中使用static关键字
- 类只能有有个静态构造函数,且不能带参数
- 静态构造函数不能有访问修饰符
其它重要点有:
- 类既可以有静态构造函数也可以有实例构造函数
- 如图静态方法,静态构造函数不能访问类的实例成员,因此不能使用this访问器
- 不能从程序中显示调用他们,系统会自动调用
5.readonly与const
字段可以用readonly修饰符声明,其作用类似于将字段声明为const,一旦值被设定就不能改版。
- const字段只能在字段的声明语句中初始化,而readonly字段可以在下列任意位置设置它的值
1.字段声明语句,类似于const 2.类的任何构造函数,如果是static字段,初始化必须在静态构造函数中完成
- const字段的值必须在编译时决定,而readonly字段的值可以在运行时决定。这种自由性允许你在不同环境或不同构造函数设置不同的值。
- const的行为总是静态的,而对于readonly字段可以使静态字段也可以是实例字段。
6.索引器和属性
索引器和属性在很多方面是相似的:
- 和属性一样,索引器不用分配内存来存储
- 索引器和属性都主要被用来访问其他数据成员,他们与这些成员关联,并为他们提供获取和设置访问。
- 属性通常表示单个数据成员,索引器通常表示多个数据成员。
- 和属性一样,索引器可以只有一个访问器,也可以两个都有
- 索引器总是实例成员,因此不能被声明为static
- 和属性一样,实现get和set的访问器代码不一定要关联某个字段或属性,这段代码可以做任何事情也可以什么都不做,只要get访问器返回某个指定类型的值即可。
索引器的声明:
ReturnType this [Type param1,...]
{
get{...}
set{...}
}
看个具体例子:
public class Indexer
{
private int Temp1;
private int Temp2;
public int this[int index]
{
get
{
return (0==index) ? Temp1 : Temp2;
}
set
{
if(0==index)
Temp1 = value;
else
Temp2 = value;
}
}
public static void Test()
{
Indexer indexer = new Indexer();
indexer.Display();
indexer[0] = 1;
indexer[1]=2;
indexer.Display();
}
public void Display()
{
Console.WriteLine($"temp1: {Temp1} temp2: {Temp2}");
}
}
7.override&new
7.1 new复写
使用new关键字可以在派生类中屏蔽基本函数(如果派生类不给new关键字,会给出警告):
public class DeriveStrategy
{
public static void Test()
{
MyDerivedClass d = new();
d.Print();
var b=(MyBaseClass)d;
b.Print();
}
}
public class MyBaseClass
{
public void Print()
=> Console.WriteLine("This is base class!");
}
public class MyDerivedClass:MyBaseClass
{
public int val;
public void Print()
=> Console.WriteLine("This is derive class!");
}
-----------------------
//ans
This is derive class!
This is base class!
从下图就可以看出:
从上图可以看出,派生类可以看到完整的对象,基类不能看到派生类成员。
7.2 override覆写
关于override的点有:
- 派生方法与基类方法有相同的签名和返回类型
- 基类的方法使用virtual标注,派生类的方法使用override标注
- 当基类调用print方法时,方法调用被传递到派生类并执行
下图阐释了这一点:
看个例子:
public class A
{
public virtual void Print() => Console.WriteLine("This is class A");
}
public class B : A
{
public override void Print() => Console.WriteLine("This is class B");
}
public class DeriveStrategy
{
public static void Test()
{
MyDerivedClass d = new();
d.Print();
var b=(MyBaseClass)d;
b.Print();
}
public static void TestOverride()
{
B b=new B();
b.Print();
var a=(A)b;
a.Print();
}
}
//运行结果
This is class B
This is class B
很显然使用override覆写和new覆盖是不一样的:当一个对象基类的引用调用一个被覆写的方法时,方法的调用被沿派生层次上溯执行,一直到标记override的方法的最高派生。
8.internal
标记为public的类可以被系统内任何程序集中的代码访问,要使一个类对其它程序集可见,使用public访问修饰符。
标记为internal的类只能被它自己的程序集的类看到,这是默认的访问级别,所以除非在类中显示指定修饰符public,否则程序集外部代码不能访问该类。
9.Switch的技巧
switch在C#7.0后可以测试任何类型,每个分支标签后面跟着一个模式表达式,该模式表达式将与测试表达式进行比较。下面看一个测试类型的例子:
public static void Test()
{
var shapes=new List<Shape>();
shapes.Add(new Circle() { Radius=7 });
shapes.Add(new Square() { Side=4});
shapes.Add(new Triangle() { Height=5});
var nullShape = (Square)null;
shapes.Add(nullShape);
foreach(var shape in shapes)
{
switch(shape)
{
case Circle circle:
Console.WriteLine("This is a circle");
break;
case Square square:
Console.WriteLine("This is a square");
break;
case Triangle triangle:
Console.WriteLine("This is a triangle");
break;
case null:
Console.WriteLine("This can be circle square triangle");
break ;
default:
throw new ArgumentException(message: "shape is not a recognise shape", paramName: nameof(shape));
}
}
}
}
public abstract class Shape { }
public class Square : Shape
{
public double Side { get; set; }
}
public class Circle:Shape
{
public double Radius { get; set; }
}
public class Triangle:Shape
{
public double Height { get; set; }
}
每个Switch等价于 :if(shape is circle)
10.多维数组例子
internal class ArrayCombo
{
public static void Test()
{
int[][,] arr = new int[3][,];
for (int i = 0; i < arr.GetLength(0); i++)
{
var temp = new int[2, 3];
for(int j = 0; j < 2;j++)
{
for(int k = 0; k < 3;k++)
{
temp[j, k] = i + j + k;
}
}
arr[i] = temp;
}
for(int i =0;i<arr.GetLength(0);i++)
{
for(int j=0;j<arr[i].GetLength(0);j++)
{
for(int k=0;k<arr[i].GetLength(1);k++)
{
Console.WriteLine($"[{i}][{j},{k}]={arr[i][j, k]}");
}
Console.WriteLine();
}
Console.WriteLine();
}
}
}
矩形数组和交错数组的结构区别非常大,例如同样是3*3的数组,在内存的结构如下:
- 两个数组都保存了9个证书,但是它们的结构却很不相同
- 矩形数组只有单个数组对象,而交错数组有4个数组对象
在CIL中,一维数组有特定的指令用于性能优化。矩形数组没有这些指令,并且不在相同级别进行优化。因此,有时使用一维数组的交错数组比矩形数组更有效率。