C#学习笔记——进阶知识点(肆)

目录

目录

一、简单数据结构类

1、ArrayList

ArrayList是C#封装好的类,本质是一个可以自动扩容的object类型的数组,提供了很多方法,如数组的增删改查

using System.Collections

ArrayList array = new ArrayList();

// 增
array.Add();

// 范围添加:把array2里的所有东西加到array后面
array.AddRange(array2);

// 插入
array.Insert(位置,元素);

// 删除(从左往右找)
array.remove(元素);

// 删除指定索引
array.removeAt(索引);

// 清除
array.Clear();

// 查看
array[索引]

// 查看元素是否存在
array.Contains(元素);

// 正向查找元素位置
array.IndexOf(元素);

// 反向查找元素位置
array.LastIndexOf(元素);

// 修改
array[索引] = 值;

// 长度、容量
array.Count;
array.Capacity;

2、Stack

Stack(栈)是C#封装好的类,本质也是object数组,但是有特殊的存储规则:先进后出

using System.Collections

Stack stack = new Stack();

// 压栈
stack.Push(元素);

// 弹栈
stack.Pop();

// 查看栈顶的内容
stack.Peek();

// 查看元素是否在栈中
stack.Contains(元素);

// 清空
stack.Clear();

3、Queue

Queue(队列)是C#封装好的类,本质也是object数组,但是有特殊的存储规则:先进先出

using System.Collections

Queue queue= new Queue();

// 入队
queue.Enqueue(元素);

// 出队
queue.Dequeue();

// 查看头部元素
queue.Peek();

// 查看元素是否在队列中
queue.Contains(元素);

// 清空
queue.Clear();

4、Hashtable

Hashtable(散列表)是基于键的哈希代码组织起来的键值对,主要作用是提高数据查询效率,使用键来访问集合中的元素

using System.Collections

Hashtable hashtable = new Hashtable();

// 增加(不能出现相同的键,值可以)
hashtable.Add(键,值);

// 删除
hashtable.Remove(键);

// 清空
hashtable.Clear();

// 查找
hashtable[键];

// 查看是否存在
hashtable.Contains(键);
hashtable.ContainKey(键);
hashtable.ContainValue(值);

// 修改
hashtable[键] = 值;

二、泛型

1、概念

泛型的核心思想是允许类或方法在定义时不指定具体的数据类型,而是在实际使用时指定

2、语法

  • 泛型类
class 类名<泛型占位符>
  • 泛型接口
interface 接口名<泛型占位符>
  • 泛型函数
返回值 函数名<泛型占位符>(参数);
  • 注意:

    • 其中泛型占位符可以有多个,用逗号分隔

    • 不确定泛型类型时,可以用default获取默认值

三、泛型约束

1、概念

可以使用泛型约束来限制在实例化泛型类型时,指定的某些类型。如果尝试使用指定约束不允许的类型实例化泛型类型,会产生编译时错误

2、语法

where 泛型字母 : 约束的类型
  • 值类型
where 泛型字母 : struct
  • 引用类型
where 泛型字母 : class
  • 存在无参公共构造函数
where 泛型字母 : new()
  • 是这个类本身或其派生类
where 泛型字母 : 类名
  • 是这个接口的派生类型
where 泛型字母 : 接口名
  • 该泛型类型是另一个泛型类型本身或者派生类型
where 泛型字母 : 另一个泛型字母

四、常用泛型数据结构类

1、List

List是C#封装好的类,本质是一个可以自动扩容的泛型类型的数组,提供了很多方法,如泛型数组的增删改查

using System.Collection.Generic

List<类型> list = new List<类型>();

// 增加
list.Add(元素);

// 范围添加
list.AddRange(另一个list);

// 插入
list.Insert(位置,元素);

// 删除
list.Remove(元素);
list.RemoveAt(索引);
list.Clear();

// 查找
list[索引]

// 查看元素是否存在
list.Contains(元素);

// 正向查找
list.IndexOf(元素);

// 反向寻找
list.LastIndexOf(元素);

// 修改
list[元素] = 值;

// 长度、容量
list.Count
list.Capacity

2、Dictionary

可理解为拥有泛型的Hashtable,也是基于键的哈希代码组织起来的键值对

using System.Collection.Generic

Dictionary<键类型,值类型> dictionary = new Dictionary<键类型,值类型>();

// 增加
dictionary.Add(键,值);

// 删除
dictionary.Remove(键);

// 清空
dictionary.Clear();

// 查找
dictionary[键]

// 查看元素是否存在
dictionary.ContainsKey(键);
dictionary.ContainsValue(值);

// 修改
dictionary[元素] = 值;

// 长度
list.Count

3、LinkedList

LinkedList是C#封装好的类,本质是一个可变类型的泛型双向链表

using System.Collection.Generic

LinkedList<int> linkedList = new LinkedList<int>();

// 往链表尾部加元素
linkedList.AddLast(元素);

// 往链表头部加元素
linkedList.AddFirst(元素);

// 在指定结点之后添加结点
linkedList.AddAfter(结点,元素);

// 在指定结点之前添加结点
linkedList.AddBefore(结点,元素);

// 移除头结点
linkedList.RemoveFirst(元素);

// 移除尾结点
linkedList.RemoveLast(元素);

// 移除指定结点
linkedList.Remove(元素);

// 清空
linkedList.Clear();

// 查找
LinkedListNode<int> first = linkedList.First;
LinkedListNode<int> last = linkedList.Last;

// 找到指定值结点
LinkedListNode<int> node = linkedList.Find(元素);

// 判断是否存在
linkedList.Contains(元素);

// 修改(结点.Value)
linkedList.First.Value = 10;

// 上一个结点
结点.Previous
// 下一个结点
结点.Next

4、泛型栈和队列

using System.Collections.Generic
    
Stack<类型>
Queue<类型>

五、顺序存储和链式存储

1、数据结构

数据结构是计算机存储、组织数据的方式和规则,是指相互之间存在的一种或多种关系的数据元素的集合。

2、线性表

线性表是一种数据结构,有n个具有相同特征的数据元素的有限序列

3、顺序存储

用一组地址连续的存储单元依次存储线性表的各个数据元素

如:数组、栈、队列、List、ArrayList

4、链式存储

用一组任意的存储单元存储线性表中的各个数据元素

如:单向链表、双向链表、循环链表

5、顺序存储与链式存储优缺点

  • 增:链式优于顺序
  • 删:链式优于顺序
  • 查:顺序优于链式
  • 改:顺序优于链式

六、委托

1、概念

委托是一种类型安全的函数指针,它允许将方法(函数)作为参数传递给其他方法。可以理解为表示函数的变量类型,可以存储和传递函数(方法)。

2、语法

  • 声明
访问修饰符 delegate 返回值 委托名(参数列表);
  • 使用
delegate void MyFun();
void Fun(){
    ...
}

// 方式一
MyFun f1 = new MyFun(Fun);
f1.Invoke();

// 方式二
MyFun f2 = Fun;
f2();
  • 委托变量可以存储多个函数
delegate void MyFun();
void Fun(){
    ...
}

MyFun f1 = Fun;
f1 += Fun;
f1 -= Fun;

3、系统自带委托

using System
    
Action  // 无参无返回值
Action<类型,类型...>  // 可以带n(不超过16)个参数的委托,有参无返回值

Func<返回值类型>   // 无参有返回值(泛型为返回值类型)
Func<参数类型,参数类型... ,返回值类型>   // 可以带n(不超过16)个参数的委托,有参有返回值

4、补充知识点

当用有返回值的委托容器存储多个函数时:

Func<string> func = () => {
    return 1; // 第一个函数
}
func += () =>{
    return 2; // 第二个函数
}
func += () =>{
    return 3; // 第三个函数
}

// 三个函数都会执行,但是返回值只能获取最后一个
Console.WriteLine(func()); // 3

如果想要获取到每一个函数执行后的返回值,使用方法GetInvocationList()可以返回一个委托数组

foreach (Func<string> del in func.GetInvocationList()){
    Console.WriteLine(del());
}

七、事件

1、概念

事件是基于委托的存在,是委托的安全包裹,是一种特殊的变量类型

2、使用

委托怎么用,事件就怎么用,但是事件和委托不同的是,事件不能在类外部赋值和调用,但外部可以添加或删除(+=-=)记录的函数。

访问修饰符 event 委托类型 事件名;

3、作用

  • 防止外部随意置空委托
  • 防止外部随意调用委托
  • 事件相当于堆委托进行了一次封装,使用更加安全

八、匿名函数

1、概念

匿名函数就是没有名字的函数,主要配合委托和事件使用

2、语法

delegate (参数列表)
{
    函数逻辑
};

九、lambda表达式

1、概念

可以理解为匿名函数的简写,都是配合委托和事件使用的

2、语法

参数类型可以省略

(参数列表) =>
{
    函数逻辑
};

3、闭包

内层函数可以引用它外层函数的变量,即使外层函数执行已停止。

class Test
{
	public event Action action;
    public Test()
    {
        int value = 10;
        action = () => {
            Console.WriteLine(value); // 闭包
        }
    }
}
  • 注意闭包陷阱:如果使用了循环创建闭包,那么内部函数使用该外部变量不是创建时的值,而是一起指向最终值。解决方法是尝试创建新变量
// for 循环的变量 i 是整个循环共享一个实例,闭包引用的是这个实例的 “引用”,而非每次循环的 “值快照”
class Test
{
	public event Action action;
    public Test()
    {
        for (int i=0;i<10;i++){
        	action += () => {
            	Console.WriteLine(i); // 闭包
        	}   
        }
    }
}

// 解决方法:为每次循环创建独立的变量副本
for (int i=0;i<10;i++){
    int index = i;
    action += () => {
        Console.WriteLine(index); // 闭包
    }   
}

十、排序

1、List自带排序

list.Sort();

2、自定义类排序

实现接口:Icomparable,补充函数CompareTo

public int CompareTo(类型 other);

返回值含义:

  • 小于0:放入other的前面
  • 等于0:保持位置不变
  • 大于0:放在other的后面

3、传入委托或匿名函数

// 委托
list.sort(MySort);

static int MySort(类型 a, 类型 b){
	...
}

// 匿名函数
list.Sort((a,b) => {return a.id > b.id ? 1 : -1});

十一、协变逆变

1、概念

  • 协变:和谐的变化,自然的变化,里氏替换原则中,父类可以装子类,所以string变成object是和谐的。
  • 逆变:逆常规的变化,不正常的变化,比如父类变子类,object变成string,感受是不和谐的。

核心思想:在类型安全的前提下,允许更灵活的泛型类型转换。

注意:逆变只是看起来像是子类装父类,但实际上还是遵循里氏替换原则的,下面会讲

2、语法

以委托为例:

  • 协变:关键字out,此时泛型只能做返回值
delegate T testOut<out T>();
  • 逆变:关键字in,此时泛型只能做参数
delegate void TestIn<in T>(T t);

3、举例

  • 协变:

首先声明了一个接口IFoo,使用out协变,因此T只能做返回值,Foo实现接口并将泛型设置为string类型。

下面的使用中,IFoo<object>可以装载IFoo<string>,因为string是object的子类,而IFoo<父类> = IFoo<子类>这种形式遵循里氏替换原则,即父类装子类,因此感受是和谐的。

调用fooObj.GetName()时,实际调用的是new Foo().GetName(),返回的是string类型,但是fooObj要求返回的是object类型,而object可以装载string,因此转换是没问题的:object name = (object) new Foo().GetName();

// 声明
interface IFoo<out T>{
    T GetName();
}

class Foo : IFoo<string>{
    public string GetName(){
        return GetType().Name;
    }
}

// 使用
IFoo<string> fooStr = new Foo();
IFoo<object> fooObj = fooStr;
object name = fooObj.GetName();
  • 逆变:

首先声明了一个接口IBar,使用in逆变,因此T只能做参数,Bar实现接口并将泛型设置为object类型。

下面的使用中,IBar<string>可以装载IBar<object>,而IBar<子类> = IBar<父类>这种形式看起来不遵循里氏替换原则,即看着像子类装父类,因此感受是不和谐的。

调用barStr.Print()时,实际调用的是new Bar().Print(),传入的是string类型,但是barObj要求传入的是object类型,而传入的string可以转换为object,因此转换是没问题的:new Bar().Print("Hello World"); // string隐式转换为object

// 声明
interface IBar<in T>{
    void Print(T content);
}

class Bar : IBar<object>{
    public void Print(object content){
        Console.WriteLine(content);
    }
}

// 使用
IBar<object> barObj = new Bar();
IBar<string> barStr = barObj;
barStr.Print("Hello World");

4、总结

  • 协变:

    • 和谐的变化,关键字out,此时泛型只能做返回值(out也是输出的意思)
    • 使用时形式类似这样:IFoo<父类> = IFoo<子类>
  • 逆变:

    • 不和谐的变化,关键字in,此时泛型只能做参数(in也是输入的意思)
    • 使用时形式类似这样:IBar<子类> = IBar<父类>
  • 核心: 协变逆变在类型安全前提下为泛型提供了灵活转换,两者都遵循里氏替换原则,逆变并不是打破了这个规则,只是表现形式不同

  • 我的简单记忆:

    • 协变是out,out是输出,只能做返回值,返回值子类可强转成父类,因此父类可以装子类,是和谐的
    • 逆变是in,in是输入,只能做参数,参数传入的子类可以强转成父类,因此子类可以装父类,表现不和谐

十二、多线程

1、进程

进程是计算机的程序关于某数据集合上的一次运行活动,是系统进行资源分配的调度的基本单位。比如打开一个应用程序就是开启了一个进程。进程之间可以相互独立运行,也可以相互访问、操作。

2、线程

操作系统能够进行运算调度的最小单位,被包含在进程中,是进程中实际运作的单位。简单理解就是一个线程就像代码运行的一条管道。

3、多线程概述

通过代码开启新的线程,多个线程同时运行就叫多线程。

4、多线程语法

  • 声明
using System.Threading;

Thread t = new Thread(MyThread);

static void MyThread(){
    线程逻辑...
}
  • 启动
t.Start();
  • 设置为后台线程
    • 当前台线程都结束时,后台线程也结束,即整个程序结束
    • 后台线程不会让进程防止被终止掉(让进程可以正常关闭)
    • 如果不设置后台线程,进程可能无法正常关闭
t.IsBackground = true;
  • 关闭释放线程
// 方式一:如果线程使用死循环,并且由标识控制,可以更改这个bool标识让线程结束
isRuning = false;

// 方式二:终止线程
t.Abort();
t = null; // 置空
  • 线程休眠
Thread.Sleep(1000); // 毫秒

5、多线程共享数据

如果多个线程同时访问一个内存区域,可能会产生冲突,可以通过加锁解决,语法lock(锁对象)

while(true){
    lock(obj){ // 加锁
        ... 访问共同区域的代码
    }
}

十三、预处理器指令

1、编译器

编译器是一种翻译程序,将源语言程序翻译为目标语言程序(如将C#源程序变成二进制的机器语言的程序)

2、预处理

预处理指令指导编译器在编译开始之前对信息进行预处理

3、常见预处理指令

  • 定义一个符号
#define
  • 取消define定义的符号
#undef
  • if流程控制
#if
#elif
#else
#endif
  • 警告和错误(一般配合#if使用)
#warning
#error

十四、反射

1、程序集(Assembly)

程序集是由编译器编译得到的,供进一步编译执行的中间产物。在Windows系统中,一般表现为.dll(库文件)或.exe(可执行文件)的格式。简单来说就是我们写的一个代码集合。

程序集是一个或多个管理代码和资源的逻辑单元。它们是C#和.NET程序结构的基本单位,也是部署和版本控制的最小单位。

2、元数据(metadata)

元数据就是用于描述数据的数据。简单来说就是程序中的类、函数、变量等信息就是程序的元数据,保存在程序集中

元数据是描述程序集内容的信息,包括类型定义、成员定义、引用的程序集等。元数据是自描述的,允许程序集在没有外部类型库的情况下进行类型检查和调用。

3、反射概念

程序运行时,可以查看它的程序集或自身的元数据。一个运行的程序查看本身或其他程序的元数据的行为就叫反射。

4、反射的作用

  • 反射可以在程序编译后获取信息,提高了程序的拓展性和灵活性
  • 程序运行时可以得到所有元数据,包括元数据的特性
  • 程序运行时可以实例化对象,操作对象

5、反射的语法

(1)Type

概念:

Type是存储类信息的类,是反射功能的基础,是访问元数据的主要方式

语法:

  • 获取Type:
// 1、万物之父object的GetType方法
int a = 1;
Type type = a.GetType();

// 2、通过typeof关键字
Type type2 = typeof(int);

// 3、通过类名(必须包含命名空间)
Type type3 = Type.GetType("System.Int32");
  • 得到类的程序集信息:
Type t = typeof(Test);
t.Assembly
  • 获取类中所有公共成员:
MemberInfo[] infos = t.GetMembers();
  • 获取类的公共构造函数:
// 获取所有构造函数
ConstructorInfo[] ctors = t.GetConstructors();
// 获取指定构造函数:传入Type数组,数组内容是参数的Type
// 无参构造
ConstructorInfo ctor = t.GetConstructor(new Type[0]);
// 有参构造
ConstructorInfo ctor2 = t.GetConstructor(new Type[] { typeof(int) });
  • 执行获取到的构造函数:
// 执行时传入object数组,表示传入的参数
// 无参构造函数
Test obj = ctor.Invoke(null) as Test;
// 有参构造函数
Test obj2 = ctor2.Invoke(new object[] { 2 }) as Test;
  • 获取和使用类的公共成员变量:
// 获取所有公共成员变量
FieldInfo[] fieldInfos = t.GetFields();
// 获取指定公共成员变量
FieldInfo ageInfo = t.GetFiled("age");
// 使用(第一个参数传入对象)
Test test = new Test();
ageInfo.GetValue(test);
ageInfo.SetValue(test,18);
  • 获取和使用类的公共成员方法:
// 获取类的所有方法
MethodInfo[] methods = t.GetMethods();
// 获取指定方法(指定方法名和参数,参数用Type数组)
MethodInfo subStr = t.GetMethod("SubStr", new Type[] { typeof(int), typeof(int) });
// 使用(使用object数组传参)
object result = subStr.Invoke("Hello World", new object[] { 5, 7 });

(2)Activator

概念:

用于快速实例化对象的类,可以将Type对象快捷实例化为对象

语法:

// 获取类的Type
Type testType = typeof(Test);
// 无参构造
Test t1 = Activator.CreateInstance(testType) as Test;
// 有参构造(第二个参数开始传入构造函数的参数)
Test t2 = Activator.CreateInstance(testType, 99) as Test;

(3)Assembly

概念:

程序集类,主要用于加载其他程序集,加载后才能用Type使用其他程序集中的信息。

如果使用不是自己的程序集的内容,需先加载程序集,如dll文件(库文件),库文件可以简单看作一种代码仓库,提供了一些可以直接用的变量、方法、类等等

语法:

  • 加载程序集
Assembly assembly = Assembly.Load("程序集名称");
Assembly assembly2 = Assembly.LoadFrom("包含程序集清单的文件名称/路径");
Assembly assembly3 = Assembly.LoadFile("加载文件的路径");
  • 获取程序集的Type
Type[] types = assembly.GetTypes();

十五、特性

1、概念

特性是一种允许我们向程序的程序集添加元数据的语言结构。是用于保存程序结构信息的某种特殊类型的类。

用于在运行时传递程序中各种元素(比如类、方法等)的行为信息的声明性标签。

简单来说可以用特性类为元数据添加额外信息,然后通过反射可以获取这些信息,进行一些特殊处理

2、自定义注解

继承基类Attribute

class MyAttribute : Attribute{
    public string info; // 特性的成员,根据需求来写
    public MyAttribute(string info){
        this.info = info;
    }
}

3、注解使用

语法: 其中参数列表为构造函数参数

[特性名(参数列表)]

可以使用的位置:

[MyAttribute("类")]
class MyClass{
    [MyAttribute("成员变量")]
    public int value;
    
    [MyAttribute("成员方法")]
    public void MyFunc( [MyAttribute("参数")] int a){
        
    }
}

4、反射获取特性

MyClass mc = new MyClass();
Type t = mc.GetType();
// 判断是否使用了某个特性(参数1:特性的Type,参数2:是否搜索继承链,即是否搜索父类)
if (t.IsDefined(typeof(MyAttribute), false)){
    Console.WriteLine("该类型使用了MyAttribute");
}

// 获取Type元数据的所有特性(参数:是否搜索继承链)
object[] array = t.GetCustomAttributes(true);

foreach (object a in array){
    if (a is MyAttribute){
        a as MyAttribute;
        ...
    }
    ...
}

5、限制自定义特性的使用范围

通过为特性类添加特性AttributeUsage来实现,参数如下:

  • AttributeTargets:特性的使用范围,能用在哪
  • AllowMultiple:是否允许多个特性实例用在同一个目标上(能否重复在同一个地方使用)
  • Inherited:特性是否能被派生类和重写成员继承
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, 
                AllowMultiple = true, 
                Inherited = true)]
public class MyAttribute : Attribute{
    
}

6、系统自带特性

  • 过时特性
Obsolete[("过时信息", 是否报错)] // 参数1:信息提示,参数2:使用时,true报错,false警告
  • 调用者信息特征
using System.Runtime.CompilerServices;
[CallerFilePath] // 指明哪个文件调用
[CallerLineNumber] // 指明哪一行调用
[CallerMemberName] // 指明哪个函数调用

实例:

public void SpeakCaller([CallerFilePath] string fileName = "",
                       [CallerLineNumber] int line = 0,
                       [CallerMemberName] string target = "")
{
    Console.WriteLine(fileName); // 输出文件路径
    Console.WriteLine(line); // 输出调用行数
    Console.WriteLine(target); // 输出调用函数
}
  • 条件编译特性

#define配合使用,加上特性后,只有使用#define定义了这个名称才可以使用

using System.Diagnostics;
[Conditional("预编译符号名称")]
  • 外部DLL包函数特性

用来标记非.Net(C#)函数,表明该函数在一个外部DLL中定义。一般用于调用C或者C++的DLL包写好的方法

using System.Runtime.InteropServices;
[DllImport("DLL包路径")]
public static extern int Add(int a,int b); //映射这个包路径的Add方法

十六、迭代器

1、概念

迭代器(iterator)有时又称光标(cursor),是程序设计的软件设计模式,迭代器模式提供一个方法顺序访问一个聚合对象的各个元素,而不暴露其内部的标识。

从表现效果看,是可以在容器对象上遍历访问的接口,设计人员无需关系内存分配的实现细节。

可以用foreach遍历的类,都是实现了迭代器的。

2、语法

接口:

using System.Collections;

IEnumerator,IEnumerable

foreach本质: 先获取in后面对象的IEnumerator(通过对象的GetEnumeratro方法),接着执行IEnumerator对象的MoveNext方法,如果返回值为true,就可以得到Current,然后赋值给item

foreach (int item in list){
    ... 处理item
}

3、使用yield return语法糖实现迭代器

所谓语法糖,主要是讲复杂的逻辑简单化,增加程序可读性。

实现IEnumerable接口后,使用yield return可以暂时返回值,保留当前状态,等到下次调用时再回来继续执行

class MyList : IEnumerable
{
    private int[] list;
    
    public MyList(){
        list = new int[] { 1,2,3,4,5,6 };
    }
    
    public IEnumerator GetEnumerator(){
        for (int i=0;i<list.Length;i++){
            yield return list[i];
        }
    }
}

十七、特殊语法

1、var隐式类型

使用var可以表示任意类型的变量,不能作为类的成员,一般用于临时声明变量时,并且必须初始化。

2、设置对象初始值

new对象时后面接上大括号可以初始化公共成员变量和属性

Person p = new Person { sex = true, Age = 18, Name = "MrTang"};

3、设置集合初始值

声明集合对象时,在new完后可以接上大括号,初始化内部属性

int array = new int[] { 1, 2, 3, 4 };
List<int> list = new List<int>() { 1, 2, 3, 4 };

4、匿名类型

匿名类型是一个由编译器临时创建的类,用于存储一组值。匿名类型只能通过var关键字来引用。

var v = new { age = 10, money = 11, name = "MrTang" };

5、可空类型和Null传播器

  • 可空类型

值类型在声明时加上?就可以赋null值,即可空类型

int? a = 3;
// 判断是否是空值
if (a.HasValue){
    a.Value;
    ..
}
// 安全获取值,为null则返回参数值,如果不填参数则返回类型的默认值
a.GetValueOrDefault(100);

  • Null传播器

引用类型在调用方法时在.前面加上?可以自动判空,如果对象为空则不执行(不报错)

object o = null;
Console.WriteLine(o?.ToString());

6、空合并操作符

语法:

如果左边值为null则返回右边值,否则返回左边值

左边值 ?? 右边值

实例:

int? v = null;
int i = v ?? 100;
// 等价于
int i = v == null ? 100 : v.value;

7、内插字符串

关键字$,用于字符串拼接

string name = "MrTang";
string str = $"好好学习,{name}";

8、单句逻辑简略写法

// 判断
if (true)
    Console.WriteLine("if省略大括号");
// 循环
for (int i = 0;i < 10; i++)
    Console.WriteLine("for省略大括号:" + i);
// 属性
public string Name{
    get => "MrTang";
    set => name = value;
}
// 函数
public int Add(int x, int y) => x + y;

十八、值类型与引用类型补充

1、分类

  • 值类型:
    • 无符号:byte,ushort,uint,ulong
    • 有符号:sbyte,short,int,long
    • 浮点数:float,double,decimal
    • 特殊:char,bool
    • 枚举:enum
    • 结构体:struct
  • 引用类型:
    • string
    • 数组
    • class
    • interface
    • 委托

2、区别

值类型和引用类型的本质区别是,值类型的具体内容存在栈内存上,引用类型的具体内容存在堆内存上。

3、变量的生命周期

编程时大部分都是临时变量,当语句块执行结束时,没有被记录的对象将被回收或变成垃圾。

值类型会被系统自动回收,引用类型在栈上的存地址的空间被系统自动回收,堆中的具体内容变成垃圾,等待下一次GC回收

4、结构体中的值和引用

结构体本身是值类型,前提是没有作为其他类的成员。

在结构体中的值,栈中存储值的具体内容;在结构体中的引用,堆中存储引用的具体内容。

5、类中的值和引用

类本身是引用类型

在类中的值,堆中存储具体的值;在类中的引用,本身存储在堆上的对象实例内部,但它指向的具体数据存储在堆的另一个位置。