☺ 观看下文前提:如果你的主语言是java,现在想再学一门新语言C#,下文是在java基础上,对比和java的不同,快速上手C#,当然不是说学C#的前提是需要java,而是下文是从主语言是java的情况下,学习C#入门到进阶。
微软c#官方文档:https://learn.microsoft.com/zh-cn/dotnet/csharp/tour-of-csharp/
c# 菜鸟教程:https://www.runoob.com/csharp/csharp-tutorial.html
开发工具:Visual Studio(VS)
C# 与.Net的关系
- C# 是由 Anders Hejlsberg 和他的团队在 .Net 框架开发期间开发的。
- C# 程序在 .NET 上运行,而 .NET 是名为公共语言运行时 (CLR) 的虚执行系统和一组类库。 CLR 是 Microsoft 对公共语言基础结构 (CLI) 国际标准的实现。
- .NET是微软公司下的一个开发平台,.NET核心就是.NET Framwork(.NET框架)是.NET程序开发和运行的环境。
- 用 C# 编写的源代码被编译成符合 CLI 规范的中间语言 (IL)。 IL 代码和资源(如位图和字符串)存储在扩展名通常为 .dll 的程序集中。 程序集包含一个介绍程序集的类型、版本和区域性的清单。
- 通过 C# 生成的 IL 代码可以与通过 .NET 版本的 F#、Visual Basic、C++ 生成的代码进行交互。
简单,现代,面向对象,类型安全,版本控制,兼容,灵活
简单:虽然 C# 的构想十分接近于传统高级语言 C 和 C++,是一门面向对象的编程语言, 但是它与 Java 非常相似 。所以它容易上手
类型安全:C# 允许动态分配轻型结构的对象和内嵌存储。 C# 支持泛型方法和类型,因此增强了类型安全性和性能。
兼容: C# 有统一类型系统。 所有 C# 类型(包括 int
和 double
等基元类型)均继承自一个根 object
类型。 所有类型共用一组通用运算。 任何类型的值都可以一致地进行存储、传输和处理。 此外,C# 还支持用户定义的引用类型和值类型。
版本控制:C# 强调版本控制,以确保程序和库以兼容方式随时间推移而变化。 C# 设计中受版本控制加强直接影响的方面包括:单独的 virtual
和 override
修饰符,关于方法重载决策的规则,以及对显式接口成员声明的支持。
using System;
namespace HelloWorldApplication
{
class HelloWorld
{
static void Main(string[] args)
{
/* 我的第一个 C# 程序*/
Console.WriteLine("Hello World");
Console.ReadKey();
}
}
}
using 关键字用于在程序中包含 System 命名空间。 一个程序一般有多个 using 语句。
命名空间声明(Namespace declaration)
最后一行 Console.ReadKey(); 是针对 VS.NET 用户的。这使得程序会等待一个按键的动作,防止程序从 Visual Studio .NET 启动时屏幕会快速运行并关闭
目录结构:
解决方案:相当于项目的工程空间,工程空间是可以有多个项目的
.vs文件: 是和项目配置相关的
后缀 .sln 文件:是解决方案文件,通过该文件可以打开解决方案
项目中后缀是 .cs 文件:就是咱用 c# 写的代码
在 C# 中,变量分为以下几种类型:
内置的引用类型有:object、dynamic 和 string。
您可以存储任何类型的值在动态数据类型变量中。这些变量的类型检查是在运行时发生的。
声明动态类型的语法:dynamic
String str = "runoob.com";
举例子2:使用@,意义:
(1)取消 \ 在字符串中的转义作用,使其单纯的表示为一个‘\’。
(2)将字符串按照编辑的原格式输出。
string str = @"C:\Windows"; //相当于string str = "C:\\Windows";
//作用2:使其按照编辑的原格式输出
Console.WriteLine(@"你好啊!
我的朋友!");
Console.ReadKey();
//输出结果为:
你好啊!
我的朋友!
C# 中的指针与 C 或 C++ 中的指针有相同的功能。语法:type* identifier;
举例子:
char* cptr;
int* iptr;
Console.WriteLine的后半部的参数变量的顺序就是对应 {0}、{1}、{2}、{3}…
举例子:
int n1 = 10;
int n2 = 20;
int n3 = 30;
Console.WriteLine("参数结果是:{0},{1},{2}", n1, n2, n3);
Console.ReadKey();
//输出结果为:
参数结果是:10,20,30
其他运算符
对于算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符 和java是一样的
其他运算符
:
运算符 | 描述 | 实例 |
---|---|---|
is | 判断对象是否为某一类型。 | If( Ford is Car) // 检查 Ford 是否是 Car 类的一个对象。 |
as | 强制转换,即使转换失败也不会抛出异常。 | Object obj = new StringReader(“Hello”); StringReader r = obj as StringReader; |
和java有点点不一样的是,default 权限在C# 被叫为internal,并且c# 多了一个访问权限为protected internal
其他的都是差不多一样的: private、protected、internal、protected internal、public
ref关键词的使用,实现参数作为引用类型,out关键字的使用,实现参数作为输出类型
☺ 输出参数的作用:方法没有返回值时,而需要从该方法中返回结果的时候,需要使用输出参数
■ ref 类型的使用,实现参数作为引用类型
:
using System;
namespace CalculatorApplication
{
class NumberManipulator
{
public void swap(ref int x, ref int y)
{
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 temp 赋值给 y */
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a = 100;
int b = 200;
Console.WriteLine("在交换之前,a 的值: {0}", a);
Console.WriteLine("在交换之前,b 的值: {0}", b);
/* 调用函数来交换值 */
n.swap(ref a, ref b);//-------这里使用了关键词ref,实现了参数作用引用类型--------
Console.WriteLine("在交换之后,a 的值: {0}", a);
Console.WriteLine("在交换之后,b 的值: {0}", b);
Console.ReadLine();
}
}
}
在交换之前,a 的值:100
在交换之前,b 的值:200
在交换之后,a 的值:100
在交换之后,b 的值:200
■ out关键字的使用,实现参数作为输出类型
:
using System;
namespace CalculatorApplication
{
class NumberManipulator
{
public void getValue(out int x )
{
int temp = 5;
x = temp;
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a = 100;
Console.WriteLine("在方法调用之前,a 的值: {0}", a);
/* 调用函数来获取值 */
n.getValue(out a);//-------这里使用了关键词out,实现了实现参数作为输出类型--------
Console.WriteLine("在方法调用之后,a 的值: {0}", a);
Console.ReadLine();
}
}
}
在方法调用之前,a 的值: 100
在方法调用之后,a 的值: 5
public 返回类型 方法名称( params 类型名称[] 数组名称 )
using System;
namespace ArrayApplication
{
class ParamArray
{
public int AddElements(params int[] arr) //-----------不定长参数----------
{
int sum = 0;
foreach (int i in arr)
{
sum += i;
}
return sum;
}
}
class TestClass
{
static void Main(string[] args)
{
ParamArray app = new ParamArray();
int sum = app.AddElements(512, 720, 250, 567, 889);
Console.WriteLine("总和是: {0}", sum);
Console.ReadKey();
}
}
}
int i; //默认值0
int? ii; //默认值null,可空的int
//-----------------------
int? i = 3; // 相当于Nullable i = new Nullable(3);
a = b ?? c //b是一个可空的值变量,如果 b 为 null,则 a = c,如果 b 不为 null,则 a = b
double? num1 = null;
double num3 = num1 ?? 5.34; // num1 如果为空值则返回 5.34
static void Main(string[] args)
{
/* 一个带有 5 行 2 列的数组 */
int[,] a = new int[5, 2] {{0,0}, {1,2}, {2,4}, {3,6}, {4,8} };
int i, j;
/* 输出数组中每个元素的值 */
for (i = 0; i < 5; i++)
{
for (j = 0; j < 2; j++)
{
Console.WriteLine("a[{0},{1}] = {2}", i, j, a[i,j]);
}
}
Console.ReadKey();
}
序号 | 方法 & 描述 |
---|---|
1 | Clear 根据元素的类型,设置数组中某个范围的元素为零、为 false 或者为 null。 |
2 | Copy(Array, Array, Int32) 从数组的第一个元素开始复制某个范围的元素到另一个数组的第一个元素位置。长度由一个 32 位整数指定。 |
3 | CopyTo(Array, Int32) 从当前的一维数组中复制所有的元素到一个指定的一维数组的指定索引位置。索引由一个 32 位整数指定。 |
4 | GetLength 获取一个 32 位整数,该值表示指定维度的数组中的元素总数。 |
5 | GetLongLength 获取一个 64 位整数,该值表示指定维度的数组中的元素总数。 |
6 | GetLowerBound 获取数组中指定维度的下界。 |
7 | GetType 获取当前实例的类型。从对象(Object)继承。 |
8 | GetUpperBound 获取数组中指定维度的上界。 |
9 | GetValue(Int32) 获取一维数组中指定位置的值。索引由一个 32 位整数指定。 |
10 | IndexOf(Array, Object) 搜索指定的对象,返回整个一维数组中第一次出现的索引。 |
11 | Reverse(Array) 逆转整个一维数组中元素的顺序。 |
12 | SetValue(Object, Int32) 给一维数组中指定位置的元素设置值。索引由一个 32 位整数指定。 |
13 | Sort(Array) 使用数组的每个元素的 IComparable 实现来排序整个一维数组中的元素。 |
14 | ToString 返回一个表示当前对象的字符串。从对象(Object)继承。 |
类是引用类型,结构是值类型。
结构不支持继承。
结构不能声明默认的构造函数。
//声明 Book 结构
struct Books
{
public string title;
public string author;
public string subject;
public int book_id;
};
//使用结构体
public class testStructure
{
public static void Main(string[] args)
{
Books Book1; /* 声明 Book1,类型为 Books */
/* book 1 详述 */
Book1.title = "C Programming";
Book1.author = "Nuha Ali";
Book1.subject = "C Programming Tutorial";
Book1.book_id = 6495407;
/* 打印 Book1 信息 */
Console.WriteLine( "Book 1 title : {0}", Book1.title);
Console.WriteLine("Book 1 author : {0}", Book1.author);
Console.WriteLine("Book 1 subject : {0}", Book1.subject);
Console.WriteLine("Book 1 book_id :{0}", Book1.book_id);
}
}
结构和类的区别:
- 1、结构是值类型,它在栈中分配空间;而类是引用类型,它在堆中分配空间,栈中保存的只是引用。
- 2、结构类型直接存储成员数据,让其他类的数据位于堆中,位于栈中的变量保存的是指向堆中数据对象的引用。
由于结构是值类型,并且直接存储数据,因此在一个对象的主要成员为数据且数据量不大的情况下,使用结构会带来更好的性能。
因为结构是值类型,因此在为结构分配内存,或者当结构超出了作用域被删除时,性能会非常好,因为他们将内联或者保存在堆栈中。
轻量对象
,假如要声明一个含有许多个颜色对象的数组,则CLR需要为每个对象分配内存,在这种情况下,使用结构的成本较低;c#中的类,类的定义-成员变量、成员方法,类的构造函数,类的实例化、调用类的成员变量、方法,都是和java一模一样的!
析构函数:
释放资源
。析构函数不能继承或重载。举例子:
using System;
namespace LineApplication
{
class Line
{
private double length; // 线条的长度
public Line() // 构造函数
{
Console.WriteLine("对象已创建");
}
~Line() //析构函数
{
Console.WriteLine("对象已删除");
}
public void setLength( double len )
{
length = len;
}
public double getLength()
{
return length;
}
static void Main(string[] args)
{
Line line = new Line();
// 设置线条长度
line.setLength(6.0);
Console.WriteLine("线条的长度: {0}", line.getLength());
}
}
}
对象已创建
线条的长度: 6
对象已删除
<访问修饰符> class <基类>
{
...
}
class <派生类> : <基类>
{
...
}
class 子类: 父类
public 子类(参数列表): base(参数列表){}
子类声明的方法{
base.父类声明的方法(参数列表);
}
using System;
namespace InheritanceApplication
{
class Shape
{
public void setWidth(int w)
{
width = w;
}
public void setHeight(int h)
{
height = h;
}
protected int width;
protected int height;
}
// 派生类
class Rectangle: Shape
{
public int getArea()
{
return (width * height);
}
}
}
//声明为接口,使用的关键词还是 interface
// 接口 PaintCost
public interface PaintCost
{
int getCost(int area);//接口中的方法,是没有任何修饰符
}
//子类实现接口,是使用冒号,代替java的implements
// 派生类
class Rectangle : PaintCost
{
public int getCost(int area)
{
return area * 70;
}
}
▷ 接口:接口是可以有属性看,但是不能有字段的!
// 接口 PaintCost public interface PaintCost { int Cost{set; get;}//接口可以有属性(属性是特殊的方法,属性的特点:是通过set设置值,通过get获取值) //public int cost2; 接口是不能有字段的 }
- 这一块属性的写法,属于简写,详细的介绍,在后边~
▷ 子类实现接口:有两种方式,常用的是隐式(子类实现接口中声明的方法时用 public),显示就是方法前有接口名:
- 接口interface实现的语法的例子就是隐式实现
// 显示实现,举例子: // 接口 PaintCost public interface PaintCost { int getCost(int area);//接口中的方法,是没有任何修饰符 } // 派生类 class Rectangle: PaintCost { int PaintCost.getCost(int area) { return area * 70; } }
abstract
修饰的方法,要加override
abstract class Shape
{
abstract public int area();
}
class Rectangle: Shape, PaintCost
{
public override int area ()
{
Console.WriteLine("Rectangle 类的面积:");
return 100;
}
}
abstract 声明的方法-抽象方法必须存在于抽象类中,抽象类中的抽象方法在声明的时候是不可以有实现体的。对于子类继承了抽象类,就必须重写人家所有的抽象方法!
而 virtual 声明的方法,没有要求一定需要存在什么类中,可以存在抽象父类、普通父类中,并且要求声明为 virtual 方法的同时,需要有实现体!对于子类继承了父类,可以重写或者不重写 父类 virtual 声明的方法。
virtual和abstract 它们有一个共同点:如果用来修饰方法,前面必须添加public,要不然就会出现编译错误:虚拟方法或抽象方法是不能私有的。
//abstract
abstract class BaseTest1
{
public abstract void fun();
}
class DeriveTest1: BaseTest2
{
public override void fun() { }//对于子类继承了抽象类,就必须重写人家所有的抽象方法!
}
//virtual
class BaseTest2
{
public virtual void fun() { }//必须有实现
}
class DeriveTest2: BaseTest1
{
//public override void fun() { } //对于子类继承了父类,可以重写或者不重写 父类 virtual 声明的方法。
}
▷重写使用的频率比较高,实现多态;覆盖用的频率比较低,用于对以前无法修改的类进行继承的时候。
多态的意思是,父类引用指向子类实例的时候。当子类重写了父类的这个方法。那么父类的引用调用是子类的方法。
//代码中的写法,多态的写法:
父亲 p = new 子类();
//接着调用同名方法
p.sameFunction();
//这时候,如果子类中和父类的同名方法,没有使用关键词override 进行修饰,那么实际上调用的是父类的方法
-------------------------------------------------- 举例子-----------------------------------------------
using System;
namespace TestApplication
{
public class Shape
{
public void ordinaryFunction()
{
Console.WriteLine(" Shape中的ordinaryFunction");
}
}
public class Rectangle: Shape
{
public new void ordinaryFunction()//子类中和父类的同名方法,没有使用关键词override 进行修饰,即没有重写的作用
{
Console.WriteLine(" Rectangle中的ordinaryFunction");
}
}
class RectangleTester
{
static void Main(string[] args)
{
Shape s = new Rectangle();//Rectangle 没有重写的情况下,调用的是父类的方法
s.ordinaryFunction();
Console.ReadKey();
}
}
}
public class Shape
{
public void ordinaryFunction()
{
Console.WriteLine(" Shape中的ordinaryFunction");
}
}
public class Rectangle : Shape
{
public new void ordinaryFunction() //重写基类的同名方法要加上new,否则会有警告
{
Console.WriteLine(" Rectangle中的ordinaryFunction");
}
}
参考:作者-北盟网校,B站视频《https://www.bilibili.com/video/BV1xP4y1y783/》,下面的文字内容就是作者-北盟网校整个视频所讲的重点了~
C++和C# 提供了namespace的概念来支持这种方式。你可以在全局的空间内指定自己的namespace,然后还可以在某个namespace内制定更小范围的namespace。
预处理器指令 | 描述 |
---|---|
#define | 它用于定义一系列成为符号的字符。 |
#undef | 它用于取消定义符号。 |
#if | 它用于测试符号是否为真。 |
#else | 它用于创建复合条件指令,与 #if 一起使用。 |
#elif | 它用于创建复合条件指令。 |
#endif | 指定一个条件指令的结束。 |
#line | 它可以让您修改编译器的行数以及(可选地)输出错误和警告的文件名。 |
#error | 它允许从代码的指定位置生成一个错误。 |
#warning | 它允许从代码的指定位置生成一级警告。 |
#region | 它可以让您在使用 Visual Studio Code Editor 的大纲特性时,指定一个可展开或折叠的代码块。 |
#endregion | 它标识着 #region 块的结束。 |
预处理器指令指导编译器在实际编译开始之前对信息进行预处理。
所有的预处理器指令都是以 # 开始。且在一行上,只有空白字符可以出现在预处理器指令之前。预处理器指令不是语句,所以它们不以分号(;)结束。
C# 编译器没有一个单独的预处理器,但是,指令被处理时就像是有一个单独的预处理器一样。在 C# 中,预处理器指令用于在条件编译中起作用。与 C 和 C++ 不同的是,它们不是用来创建宏。一个预处理器指令必须是该行上的唯一指令。
C# 中的异常类主要是直接或间接地派生于 System.Exception 类。System.ApplicationException 和 System.SystemException 类是派生于 System.Exception 类的异常类。
System.ApplicationException 类支持由应用程序生成的异常。所以程序员定义的异常都应派生自该类。
System.SystemException 类是所有预定义的系统异常的基类。
用于字段和方法接入时要远慢于直接代码
。因此反射机制主要应用在对灵活性和拓展性要求很高的系统框架上,普通程序不建议使用。程序员希望在源代码中看到程序的逻辑,反射却绕过了源代码的技术
,因而会带来维护的问题,反射代码比相应的直接代码更复杂。Net 框架提供了三种预定义特性:
▪ 预定义特性 AttributeUsage 描述了如何使用一个自定义特性类。它规定了特性可应用到的项目的类型。
▪ 规定该特性的语法如下:
[AttributeUsage(
validon,
AllowMultiple=allowmultiple,
Inherited=inherited
)]
AttributeTargets
的值的组合。默认值是 AttributeTargets.All。[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]
// 声明类型为 string 的 Name 属性
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
// 简写为:
public string Name {get; set;}
using System;
namespace runoob
{
public abstract class Person
{
public abstract string Name{get; set;}
public abstract int Age{get; set;}
}
class Student : Person
{
private string name = "N.A";
private int age = 0;
// 声明类型为 string 的 Name 属性
public override string Name
{
get
{
return name;
}
set
{
name = value;
}
}
// 声明类型为 int 的 Age 属性
public override int Age
{
get
{
return age;
}
set
{
age = value;
}
}
public override string ToString()
{
return Name = " + Name + ", Age = " + Age;
}
}
}
指针
!即用变量调方法
。好处:使方法的使用变得更加灵活。public|internal delegate 返回值 委托名(参数列表);
//声明为委托类型
delegate int NumberChanger(int n);
--------------------------------------------------------------------
public static int AddNum(int p)
{
num += p;
return num;
}
--------------------------------------------------------------------
// 使用new 创建委托实例 或者直接使用变量指向方法
NumberChanger nc1 = new NumberChanger(AddNum);//相当于 NumberChanger nc1 = AddNum;
// 使用委托对象调用方法,执行AddNum 方法
nc1(25);
using System;
delegate int NumberChanger(int n);
namespace DelegateAppl
{
class TestDelegate
{
static int num = 10;
public static int AddNum(int p)
{
num += p;
return num;
}
public static int MultNum(int q)
{
num *= q;
return num;
}
public static int getNum()
{
return num;
}
static void Main(string[] args)
{
// 创建委托实例
NumberChanger nc;
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
nc = nc1;
nc += nc2;
// 调用多播
nc(5);
Console.WriteLine("Value of Num: {0}", getNum());
Console.ReadKey();
}
}
}
Value of Num: 75
看看前面的委托的声明的语法,声明为委托类型的时候,修饰符是 public|internal
而有时候,程序为了安全考虑,需要私有化,即封装起来,不给外界随意调用,随意修改!
委托链可以累加方法,+= 误用了 = 那么只会执行最后一个=的方法,不安全,有被覆盖的隐患
事件实际上是一个私有委托变量,对外界开放了一个向委托变量增加方法绑定的方法,和开放了一个减少委托变量身上绑定的方法。
事件是一种特殊的多播委托,仅可以从声明事件的类(或派生类)或结构( 发布服务器类
)中对其进行调用。
如果其他类或结构订阅该事件,则在发布服务器类引发该事件时,将调用其事件处理程序方法。
event
关键字用于声明发布服务器类中的事件。
//关于如何声明和引发使用 [EventHandler] 作为基础委托类型的事件
public class SampleEventArgs
{
public SampleEventArgs(string text) { Text = text; }
public string Text { get; } // readonly
}
public class Publisher
{
// Declare the delegate (if using non-generic pattern).
public delegate void SampleEventHandler(object sender, SampleEventArgs e);
// Declare the event.
public event SampleEventHandler SampleEvent;
// Wrap the event in a protected virtual method
// to enable derived classes to raise the event.
protected virtual void RaiseSampleEvent()
{
// Raise the event in a thread-safe manner using the ?. operator.
SampleEvent?.Invoke(this, new SampleEventArgs("Hello"));
}
}
using System;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Counter c = new Counter(new Random().Next(10));
c.ThresholdReached += c_ThresholdReached;//真实的事件处理方法
for(int i = 0; i < 10; i++)
{
Console.WriteLine("adding one");
c.Add(1);
}
}
/**
* 真实的事件处理方法
*/
static void c_ThresholdReached(object sender, ThresholdReachedEventArgs e)
{
Console.WriteLine("The threshold of {0} was reached at {1}.", e.Threshold, e.TimeReached);
Environment.Exit(0);
}
}
/**
* 事件参数类
* EventArgs: 是C# 内置的类,内部有一个委托属性 public delegate void EventHandler(object? sender, TEventArgs e);
*/
public class ThresholdReachedEventArgs : EventArgs
{
public int Threshold { get; set; }
public DateTime TimeReached { get; set; }
}
class Counter
{
//声明事件
public event EventHandler ThresholdReached;
private int threshold;
private int total;
public Counter(int passedThreshold)
{
threshold = passedThreshold;
}
public void Add(int x)
{
total += x;
if (total >= threshold)
{
ThresholdReachedEventArgs args = new ThresholdReachedEventArgs();
args.Threshold = threshold;
args.TimeReached = DateTime.Now;
OnThresholdReached(args);//监听事件
}
}
/**
*监听事件
*/
protected virtual void OnThresholdReached(ThresholdReachedEventArgs e)
{
EventHandler handler = ThresholdReached;
if (handler != null)
{
handler(this, e);//事件处理程序
}
}
}
}
以日志事件为例,日志事件写法:public event EventHandler LogHandler; 它的的本质如下:
private delegate EventHandler LogHandler;
public event EventHandler LogHandler
{
add
{
LogHandler += value;//添加到事件的方法队列中
}
remove
{
LogHandler -= value;
}
}
事件通常用于表示用户操作,例如单击按钮或图形用户界面中的菜单选项。
发行者确定何时引发事件;订户确定对事件作出何种响应。
一个事件可以有多个订户。 订户可以处理来自多个发行者的多个事件。
没有订户的事件永远也不会引发。
当事件具有多个订户时,引发该事件时会同步调用事件处理程序。
在 .NET 类库中,事件基于 EventHandler 委托和 EventArgs 基类。
当一个代码块使用 unsafe 修饰符标记时,C# 允许在函数中使用指针变量。不安全代码或非托管代码是指使用了指针变量的代码块。
如果本文对你有帮助的话记得给一乐点个赞哦,感谢!