C#基础语法的学习

学习基础

已经学过一门面向对象的语言和C语言基础

本文按找基本介绍,物质,运动,类的顺序进行介绍

.NET 概述

什么是 .NET

.NET(dotnet)是微软公司发布的应用程序框架 ,用以减轻软件开发人员的工作 .它包括一系列类库、运行时等内容 .

在生成一个 .NET 程序时 ,代码翻译成微软中间语言 (MSIL, Microsoft Intermediate Language)的可执行文件 .

执行该可执行文件时 ,将启动对应 .NET 框架的“公共语言运行时 (CLR, Common Language Runtime)” ,由该 CLR 将 MSIL 编译为机器码执行 ,称作 JIT 编译 (just-in-time compilation) .

所以,由.net开发的程序的优点

  1. 可以跨平台(使用不同的CLR编译)
  2. 可以通过一些工具,如dnspy,反编译出其源代码
  3. 轻松实现垃圾回收 (GC, Garbage Collection)

.NET历史

.NET Framework: 微软推出的框架名字叫“.NET Framework” ,闭源且只能在 Windows 上运行

.NET Core: 后来,微软拥抱开源与跨平台 ,推出了“.NET Core”框架 ,该框架开源 ,并且支持多种操作系统 ,

.NET: 并且随着 .NET Core 版本的更迭 ,.NET Framework 支持的功能也逐渐向 .NET Core 迁移 .当 .NET Core 3.1 出现后 ,功能已经接近完备 ,接近甚至超过了 .NET Framework ,微软决定下个版本扔掉 Core ,下个版本直接改名“.NET” ,与 .NET 同名以表明它将是以后 .NET 的主要发展方向 .况且考虑到 .NET Framework 最新版本已经是 4.x ,因此为了防止发生混淆 ,.NET 版本跨过 4.x ,而直接于 2020 年下半年推出 .NET 5 ,并在之后每年推出一个 .NET 版本 .

版本 : 对于 .NET 来说 ,偶数版本的 .NET为长期支持的 LTS 版本 (Long Term Support) ,支持期限为三年;奇数版本的 .NET为标准支持的 STS 版本 (Standard Term Support) ,支持期限为 18 个月 .

规范 : 在 .NET Core 出现之前 ,开源社区 (非微软官方)自己创造了一个支持 C# 语言的跨平台运行时 ,称作“mono” .为了便于管理如此繁杂的 .NET 体系 ,微软推出了 .NET Standard 规范 ,作为各个 .NET 运行时遵循的准则 .

.NET 现在可以支持多种语言或被多种语言进行调用 ,例如 C#、Visual Basic.NET、F#、PowerShell、C++/CLI ,等等 .

第一个 C# 程序

Main方法

创建VS控制台程序后,打开 Program.cs ,我们看到下面代码

一般按找这个规范来编写代码,因为CS开发经常涉及很多源文件,使用命名空间和类

形成如下良好的编码规范,让我们的程序和思维更清晰

using System;

namespace hello
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}
//等价于System.Console.WriteLine("Hello World!"); 

代码规范

单一入口点

C#其实可以不要Main方法,但为了我们程序流可追踪,有单一入口.

所以一般来说 ,习惯上都定义且只定义一个 Main 方法在 Program 类中 .

命名

大驼峰 : 类名,方法,接口(并且以I开头)

小驼峰 : 字段(变量名)

物质

类型系统

C# 是一种面向对象的强类型语言,具有一个庞大的类型系统 .

C# 中一切类型(除指针类型)均继承自 object (System.Object)类

C# 的类型分为两种: 值类型和引用(指针)类型 .

值类型

值类型是一类比较简单的类型,直接在内存栈上或其他内存位置储存数值,都是由System.ValueType 派生而来

数值类型

,均属于结构类型 ,C# 内置的数值类型有:

C# 类型名称 范围 对应的 .NET 类型 备注
sbyte -128 ~ 127 System.SByte 8 位有符号整数
byte 0 ~ 255 System.Byte 8 位无符号整数
short -32,768 ~ 32,767 System.Int16 16 位有符号整数
ushort 0 ~ 65535 System.UInt16 16 位无符号整数
int -2,147,483,648 ~ 2,147,483,647 System.Int32 32 位有符号整数
uint 0 ~ 4,294,967,295 System.UInt32 32 位无符号整数
long System.Int64 64 位有符号整数
ulong 0 ~ 18,446,744,073,709,551,615 System.UInt64 64 位无符号整数
nint 取决于平台 System.IntPtr 32 或 64 位有符号整数
nuint 取决于平台 System.UIntPtr 32 或 64 位无符号整数
float System.Single IEEE754 单精度浮点数
double System.Double IEEE754 双精度浮点数
decimal ±1.0E-28 ~ ±7.9228E28 System.Decimal 16 个字节小数
bool true, false System.Boolean 布尔类型 ,1 字节
char U+0000 ~ U+FFFF System.Char Unicode UTF-16 字符类型;2 字节
结构
public struct Point
{
    public int x;
    public int y;
}
枚举
public enum Color
{
    Red = 0,
    Blue = 1,
    Yello = 2,
    Purple = 8,
    Black
}

引用类型

因为全是对象,而对象包含数据多,在传递时就会消耗大量性能

所以出现引用,即指向数据的地址

我们都是直接传递地址,而非实际数据

当一个数据没有引用时,就会被释放,即垃圾回收

数组

数组属于引用类型.数组均继承自 System.Array 类 .

一维数组

定义一个一维数组的最基本形式如下:

// type[] arr = new type[array_size];
int[] arr1 = new int[5];                    // 定义一个长度为 5 的数组 ,每个元素初始值均为默认值 0
int[] arr2 = new int[5] { 1, 2, 3, 4, 5 };  // 定义一个长度为 5 的数组 ,初始值分别为 1, 2, 3, 4, 5
var arr3 = new int[] { 1, 2, 3, 4, 5 };     // 数组的长度可以由编译器自动推导
int[] arr4 = { 1, 2, 3, 4, 5 };             // 简略写法

其中 ,各个数组的类型均为 int[] .可以通过 [] 访问其元素 ,Length 获取元素个数:

for (int i = 0; i < arr1.Length; ++i)
{
    Console.WriteLine(arr1[i]);
}

也可以使用 foreach 语句遍历:

foreach (var i in arr1)
{
    Console.WriteLine(i);
}

注:通过 foreach 是无法修改数组中的元素的 ,只能访问其值 .

多维数组

C# 可以定义多维的数组 ,以二维数组为例 ,二维数组的类型为 int[,]:

int[,] arr1 = new int[2, 3] { { 1, 2, 3 }, { 5, 4, 9 } };
int[,] arr2 = { { 1, 2, 3 }, { 5, 4, 9 } };

通过 Length 获取数组的长度 ,通过 GetLength(n) 获取数组第 n 维的长度:

var arr = new int[,] { { 1, 2, 3 }, { 5, 4, 9 } };
Console.WriteLine($"{arr.Length} {arr.GetLength(0)} {arr.GetLength(1)}");

输出:

6 2 3
交错数组

C# 支持嵌套的数组 ,即数组的每个元素都是一个数组 .这样的数组称为“交错数组” .以一个每个元素都是一维数组的一维交错数组为例 (其他维度的数组类似):

int[][] arr = new int[2][]
   {
        new int[3]{ 1, 2, 3 },
        new int[2]{ 4, 5 }
   };

上述定义了一个具有两个元素的交错数组 .

需要注意的是 ,数组也是引用类型 ,因此交错数组的每个元素都要用 new 产生 ,例如下面的:

int[][] arr = new int[2][];

此处 arr 具有两个元素 ,每个元素都是一个 int[] 的引用 .但是由于并没有用 new 为每个元素创建托管对象 ,因此每个引用都是 null ,并没有指向任何数组 .·

运动

输入输出

输出

System.Console.Write("Hello, ");
System.Console.WriteLine("world!");//会自动添加换行

格式化输出

int x = 4, y = 5, z = 6;
Console.WriteLine($"z = {z}, x = {x}, y = {y}"); // {n} 代表该处应换成第 n 个参数的值

输入

格式化输入

string s = Console.ReadLine();
int x = Convert.ToInt32(Console.ReadLine());
double d = Convert.ToDouble(Console.ReadLine());

运算与控制

判断null

  • ? foo不为空则执行方法(就可以不写if来判断了)

    foo?.DoSomething();
    
  • ?? s为空则输出后者

    Console.WriteLine(s ?? "Null string");
    
  • ??= o为空则赋值

    o ??= new object();
    

控制

  • 条件分支:ifswitch
  • 循环:whiledo...whileforforeach
  • 分支:goto

基础

访问修饰符

  • private: 这个字段只能够被本类所访问
  • public:这个字段可以被随意访问
  • protected:这个字段可以被本类及其派生类访问
  • internal:这个字段可以在本程序集内随意访问
  • protected internal:既可以被本类及其派生类访问 ,又可以在本程序集内随意访问
  • private protected:可以被本类及其在本程序集内的派生类访问

字段(成员变量)

一个类可以含有它自己的字段 (field)

可设置默认值,不设置则为0或null

class Person
{
    private int age=18;
    private string name;
}
this

指向自己的引用

静态字段

静态字段可以不需要类的实例

class Person
{
    public static int population;
}
Console.WriteLine(Person.population);
常量字段

常量字段与静态字段一样 ,只能通过“类名.字段名”来访问 .

class MathTool
{
    public const double PI = 3.1415926535897932384626;
}

属性

为了封装性,字段应该都通过setter和getter获取值

为了简化这种操作,C#提供了属性的语法

属性是对字段的封装

手动定义

如下 ,Age 是一个属性 ,它用于设置和访问 age 字段的值 .

class Person
{
    private int age;
    public int Age
    {
        get { return age; } // 或 get => age;
        private set { age = value >= 0 ? value : 0; }
    }
    public Person(int age_)
    {
        Age = age_;  // 调用 set 访问器
    }
    public void AddAge()
    {
        ++Age;  // 调用 set 和 get 访问器 ,等价于 Age = Age + 1;
    }
}
自动实现

因为这种成对出现,字段其实就没什么写出来的必要了

C#提供了自动属性的功能,即我们定义属性就行,会隐式生成字段绑定到属性上

class Complex
{
    public double Real { get; set; }
    public double Imag { get; set; }
}
readonly

在字段前加上 readonly 修饰符代表该字段只能在构造方法里被赋值 .一旦构造方法里被赋值后 ,便不能再修改它的值:

class WebPage
{
    private readonly string url;
    public WebPage(string url)
    {
        this.url = url;  
    }
    private void Test()
    {
        // url = "http://www.4399.com"; // 编译错误!url 是只读的!
    }
}
init

类似于只读字段,只能在构造函数内设置,属性的也有类似的特性,将 set 改成 init ,就可以达到该目的

class WebPage
{
    public string Url { get; private init; }
    public WebPage(string url)
    {
        Url = url;  
    }
    private void Test()
    {
        // Url = "http://www.4399.com"; // 编译错误!
    }
}

方法(成员函数)

与其他语言不同,c#的函数都是封装在类中的

类内可以含有方法 (method)

class Person
{
    private int age = 0;
    private string name = "Tom";

    public void Print(int n)
    {
        for (int i = 0; i < n; ++i)
        {
            Console.WriteLine($"age: {age}, name: {name}");
        }
    }
}

一个类的方法需要用“对象名.方法名”的方式调用 ,例如:

var ps = new Person();
ps.Print(2);
静态方法

同静态变量,不需要实例化

class MathTool
{
    static public int Add(int x, int y)
    {
        return x + y;
    }
}
Console.WriteLine(MathTool.Add(3, 5)); // 输出 8
引用传参

因为值类型需要通过方法修改值,则可以通过引用传参实现

方法,在类型前加上ref修饰

using System;

public class Program
{
    
    
    public static void Main()
    {
    
    
        int a = 5;
        int b = 10;
        Console.WriteLine($"Before swap: a = {
      
      a}, b = {
      
      b}");
        Swap(ref a, ref b);
        Console.WriteLine($"After swap: a = {
      
      a}, b = {
      
      b}");
    }
    public static void Swap(ref int x, ref int y)
    {
    
    
        int temp = x;
        x = y;
        y = temp;
    }
}
输出参数

参数除了以 ref 方式传递外 ,有时我们需要用参数来返回值 .

这时虽然也可以用 ref 关键字来达到目的 ,但是最好使用 out 关键字 ,用法与 ref 完全相同 .

只不过使用 out 的时候编译器会进行编译器检查 ,是否真的给该参数赋了值 ,并且不能未经赋值便获取该参数的值 .

也可以给参数加上 in 关键字修饰以强调该参数是输入参数,知识增加代码可读性.

默认参数

C# 具有参数缺省值:

class MathTool
{
    static public int Div(int x = 1, int y = 1)
    {
        return x / y;
    }
}
简化函数体

如果函数体非常简短 ,可以使用 => 运算符:

class MathTool
{
    static public int Add(int x, int y) => x + y;
}
扩展方法

C# 可以为一个类定义扩展方法 .即一个类定义好后 ,可以在类外为这个类定义额外的方法 ,被扩展的类需要作为第一个参数 ,并用 this 标识:

static public void Output(this string s, int times)
{
    for (int i = 0; i < times; i++)
    {
        Console.WriteLine(s);
    }
}

我们为 string 类定义了一个扩展方法 ,之后我们可以像使用平常的方法一样使用扩展方法:

string s = "Hello";
s.Output(5);

特殊方法

构造方法

每个类可以定义构造方法 .构造方法不具有返回值 ,且方法名与类名相同 ,是在一个对象被构造的时候调用的方法 ,由 new 表达式传递参数:

class Person
{
    private int age;
    public Person(int age_)
    {
        age = age_;
    }
}
Person ps = new Person(4);
析构函数

本名终结器 (finalizer) ,是类的一个方法 ,一个类只能有一个终结器 ,且不能继承或重载 ,而是在垃圾回收器在回收该对象的内存的时候调用的 .

终结器的名字是类名前加上波浪线 ~ ,例如:

class Foo
{
    ~Foo() // 终结器
    {
        
    }
}

重载

方法的重载

一个类里可以定义多个同名方法 ,但是它们的参数列表必须不同 .调用时根据传递的实参决定调用哪个重载方法 .

运算符重载

一些运算符可以进行重载 ,例如:+-*&|truefalse ,等等 .方法是将运算符 (设为 op)其定义成方法 operator op .需要注意的是 ,运算符重载只能将运算符重载为静态方法 ,而不能是非静态方法 ,例如计算向量内积:

namespace Math
{
    public class Vector2
    {
        public double X { get; private set; }
        public double Y { get; private set; }
        public Vector2(double x, double y)
        {
            this.X = x;
            this.Y = y;
        }
        public static double operator*(Vector2 v1, Vector2 v2)
        {
            return v1.X * v2.X + v1.Y * v2.Y;
        }
    }
}

继承

C# 继承的语法如下:

public class Base {}
public class Derived : Base {}

Derived 类继承自基类 Base .如果一个类没有显式继承另一个类 ,那么它默认继承自 object 类 .

C# 不支持类的多继承 ,即一个类有且仅有一个基类 (object 类除外) .

在类里可以通过 base 关键字代表它的基类 ,同样构造方法也需要通过 base 关键字来为它的基类提供构造方法的参数 .

class Animal
{
    private string name;
    public Animal(string name)
    {
        this.name = name;
    }
}
class Dog : Animal
{
    public Dog(string name) : base(name) {}
}

密封类

密封类用 sealed 标识 .密封类不可再被继承 ,即被声明为 sealed 的类不能再派生任何类 ,例如:

public sealed class Foo {}

is 运算符

.NET 支持在运行期进行类型检查 .使用 is 运算符可以检查对象的类型 ,例如:

// 变量声明:
// Animal ani = new Animal("");
// Dog dog = new Dog("");
// Animal ani2 = dog;  // 用 ani2 引用指向 dog 指向的对象
// object o = 1;   // 将整数 1 装箱

Console.WriteLine(ani is Animal);   // True ,ani 是 Animal 类的对象
Console.WriteLine(ani is Dog);      // False ,ani 不是 Dog 类的对象
Console.WriteLine(dog is Animal);   // True ,Animal 是 Dog 类的基类
Console.WriteLine(ani2 is Dog);     // True ,ani2 指向的确实是 Dog 类的对象
Console.WriteLine(o is int);        // True ,拆箱

多态

抽象类

即预定义了方法,需要继承后完善,如下的call(),可用abstract和virtual修饰

public abstract class Animal
{
    public abstract void Call();
}

public sealed class Dog : Animal
{
    public override void Call()
    {
        Console.WriteLine("Wang wang!");
    }
}
Animal ani = new Dog();
ani.Call();                     // 输出 Wang wang
// Animal ani2 = new Animal();  // 编译错误 ,Animal 是抽象类

接口

C# 支持接口 (interface),不含字段的类,用interface定义

着重于数据处理

public interface ICallable
{
    void Call();
}

public class Cat : ICallable
{
    void ICallable.Call()
    {
        Console.WriteLine("Meow");
    }
}

泛型(模板)

C# 支持泛型 .可以创建泛型类、泛型方法、泛型接口、泛型委托、泛型记录 ,等等 .

泛型类和泛型方法的定义方法很简单 ,只需要在类或方法名后使用尖括号 <> 括住泛型的名称即可:

class Point<T>
{
    public T X { get; private set; }
    public T Y { get; private set; }
    public Point(T x, T y)
    {
        this.X = x;
        this.Y = y;
    }
}
Point<int> pt = new Point(0, 0);

委托

委托提供了一种有效的机制来实现调用和事件处理定义、实例化

自定义

需要用 delegate 关键字定义一个委托类型 .委托类型的定义格式与方法类似 ,只是在返回值类型前加上 delegate 关键字:

// int Add(int a, int b) => a + b;  // Add 方法的定义
delegate int BinaryFunctor(int x, int y); 	//定义委托
BinaryFunctor bf = new BinaryFunctor(Add); 	//赋值委托
Console.WriteLine(bf(3, 5)); 			   //使用委托

内置委托

多数情况下 ,我们并不需要自定义委托类型 ,.NET 中已经定义好了一些内置的委托类型:

  • Action 是返回值为 void 类型的委托 ,泛型参数列表内为参数列表 ,例如 Action 为无参且返回值为 void 的委托、Action<int> 为参数是 int 且返回值为 void 的委托 .Action 最多可以有 16 个参数类型 .
  • Func 是既有参数又有返回值的委托 .泛型参数列表中最后一个为返回值类型 .例如 Func<int, double> 为参数是 int、返回值是 double 的委托 .Func 最多可达 16 个参数类型和 1 个返回值类型 .

多播委托

一个委托不仅可以绑定一个方法 ,还可以绑定多个方法 .这种委托我们称为“多播委托” .

可以用 +- 来将方法从委托中附加或删除

// static public void Call1() => Console.WriteLine("Call1");
// static public void Call2() => Console.WriteLine("Call2");
// static public void Call3() => Console.WriteLine("Call3");

var caller = new Action(Call1);
caller += Call2;
caller = caller + Call3;
caller.Invoke(); // 等价于 caller();

lambda 表达式

lambda 表达式可以看成是一个匿名方法 .语法很简单 ,例如一个求和的 lambda 表达式:

(x, y) => {x + y;

lambda 表达式可以直接当作方法赋值给委托 ,例如:

var output = new Action
(
	() => Console.WriteLine("Hello, world!");
);
var getAddOne = new Func<int, int>(x => x + 1);

lambda 表达式可以自动推导出 ActionFunc 类型 ,也可以手动指定参数类型和返回值类型 ,因此上述代码可以用以下更简洁的方式写出:

var output = () =>
{
    Console.WriteLine("Hello, world!");
};
var getAddOne = (int x) => x + 1;

以及下面的代码:

var f = object (int x) => x + 1; // f 是 Func<int, object> 而非 Func<int, int>

事件

如捕获键盘输入,这种多可能事件,并对不同事件添加不同方法

using System;

public class Publisher
{
    
    
    public delegate void MyEventHandler(string message);
    public event MyEventHandler MyEvent;
    public void RaiseEvent(string message)
    {
    
    
        MyEvent?.Invoke(message);
    }
}

public class Subscriber
{
    
    
    public void HandleEvent(string message)
    {
    
    
        Console.WriteLine($"Event handled: {
      
      message}");
    }
}

class Program
{
    
    
    static void Main()
    {
    
    
        Publisher publisher = new Publisher();
        Subscriber subscriber = new Subscriber();
        
        publisher.MyEvent += subscriber.HandleEvent;
        
        publisher.RaiseEvent("Hello, world!");
    }
}

异常处理

C# 支持异常处理 .异常处理由一个 try 语句块加上至少一个 catchfinally 语句块组成:

try
{
    /*Some code*/
}
/*catch / finally*/

举一个简单的例子 ,ExceptionSystem 命名空间的一个类 ,是一切异常类的基类 ,抛出的异常也必须从 Exception 类中派生出来 .Message 是该类的虚属性 ,通常储存着异常信息 .

try
{
    /*Some code 1*/
    throw new Exception("Throw an exception!");
    /*Some code 2*/
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

上述代码中 ,“Some code 1”执行完毕后 ,将会执行 throw 语句 ,实例化一个 Exception 对象 ,并抛出 .然后被下面的 catch 捕获到 ,执行 catch 块内的语句 .

try 块下可能有多个 catch 块 ,则抛出异常时会一次查找下面每个 catch 块所捕获的内容 .如果找到了可以匹配的类型 ,则执行该 catch 块 ,如果没找到则异常向上抛出 .

单独的一个 catch 可以捕获全部异常 ,单独的一个 throw 可以将捕获的异常再次抛出:

try {}
catch  // 捕获全部异常
{
    Console.WriteLine("Caught");
    throw; // 将捕获到的异常再次抛出
}

C# 支持 finally 语句块 ,放在所有 catch 块之后 .无论是否抛出了异常、是否执行了 trycatch ,在执行完全部的 trycatch 后 ,都将进入 finally 块执行 ,然后再执行其他工作 .因此 finally 块常用于进行一些恢复或清理的工作 .

// var rwlock = new ReaderWriterLockSlim();
rwlock.EnterWriteLock();    // 锁住
try
{
    /* Some code */
}
finally
{
    rwlock.ExitWriteLock();   // 解锁
}

非托管资源与 IDisposable

有时 ,我们使用的资源并不都是托管的 ,我们需要手动管理这些非托管资源 .例如我们在与一些底层的应用程序接口 (API)进行交互的时候 ,我们必须要考虑资源泄露的问题 .这时候 ,我们可以让类继承 System.IDisposable 接口 ,实现接口中的 Dispose() 方法来达到目的 .

partial class DeviceContext : IDisposable
{
    public void Dispose()
    {
        /*Release resources*/
    }
}

之后我们实例化 DiviceContext 时 ,便可以使用 using:

public void Draw()
{
    using var dc = new DeviceContext();
    /*Use dc to draw pictures*/

    // 函数退出前调用 Dispose 方法
}

这时 ,编译器会把上述代码展开成下面代码的等效代码:

public void Draw()
{
    DeviceContext? dc = null;
    try
    {
        dc = new DeviceContext();
        /*Use dc to draw pictures*/
    }
    finally { ((IDisposable)dc)?.Dispose(); }
}

如果想让资源在函数中释放而不是函数退出时:

public void Draw()
{
    // Do something
    using (var dc = new DeviceContext())
    {
        /* Use dc to draw pictures */

        // 跳出 using 块前调用 Dispose 方法
    }
    // Do something
}

这样我们就能有效管控非托管资源 .

.NET 数据结构

.NET 提供了很多数据结构可供使用:

System.Collections

位于 System.Collections 命名空间中的集合的每个元素都是 object (少数集合如 BitArray 除外) ,这意味着它可以容纳任何类型:

  • ArrayList:列表 (可变长的数组 ,线性结构)
  • SortedList:有序列表
  • Stack:栈
  • Queue:队列
  • Hashtable:哈希表 (键值对)

System.Collections.Generic

位于 System.ollections.Generic 命名空间中的集合都是泛型集合 ,其容纳的元素类型由泛型参数决定:

  • List<T>:列表 (可变长的数组 ,线性结构)
  • SortedSet<T>:有序列表
  • SortedList<T>:有序列表 (键值对)
  • LinkedList<T>:双向链表
  • Stack<T>:栈
  • Queue<T>:队列
  • PriorityQueue<T>:优先级队列
  • HashSet:哈希集合
  • Dictionary:字典 (键值对)
  • SortedDictionary:有序字典 (键值对 ,二叉搜索树)

System.Collections.Concurrentnt)

位于 System.Collections.Concurrent 命名空间中的集合时并发安全的 ,将在其他文档中进行介绍 .

猜你喜欢

转载自blog.csdn.net/killsime/article/details/135212121