C#学习笔记

编译执行C#程序

启动VS----选择C#控制台应用----命名----框架选择.NET6或者7----出现一个类----点击run或F5运行。

using System;
namespace HelloWorldApplication
{
class HelloWorld
{
static void Main(String[] args)
{
/*初学C#,请多指教!*/
Console.WriteLine("Hello World!");
Console.ReadKey();
}
}
}

using 关键字用于在程序中包含 System 命名空间,一个程序一般有多个 using 语句。

namespace 命名空间声明,一个namespace 中包含了一系列的类;在这里 HelloWorldApplication 命名空间包含了类 HelloWorld。

class 声明,类一般包含多个方法,方法定义了类的行为。类 HelloWorld 包含了程序使用的数据和方法声明。在这里 HelloWorld 类中只有一个 Main 方法。

Main方法是所有C#程序的入口点,Main方法说明当执行时类将做什么动作。

/**/为注释,会被编译器忽略。

Main方法通过语句 Console.WriteLine("Hello World");指定了它的行为。WriteLine是一个定义在System命名空间中的Console 类的一个方法,该语句会在控制台输出语句。

Console.ReadKey();是针对VS.NET用户,使得程序会等待一个按键的动作,防止VS.NET启动时屏幕会快速运行并关闭。

C#几点注意:

  • C#大小写敏感
  • 所有语句和表达式必须以分号结尾
  • 程序的执行从Main方法开始
  • 不同于Java ,C#文件名可以用于不同类的名称

C#是一种面向对象的编程语言,在面向对象的程序设计方法中,程序由各种相互交互的对象组成,相同种类的对象通常具有相同的类型,或者说,是在相同的 class 中。

using System;
namespace RectangleApplication
{
class Rectangle
{
//----单行注释-----
//成员变量
double length;
double width;
//成员函数
public void Acceptdetails()
{
length = 4.5;
width = 3.5;
}
public double GetArea()
{
return length * width;
}
public void Display()
{
Console.WriteLine("Length:{0}", length);
Console.WriteLine("Width:{0}", width);
Console.WriteLine("Area:{0}", GetArea());
}
}
//实例化的类
class ExecuteRectangle
{
static void Main(string[] args)
{
Rectangle r = new Rectangle();
r.Acceptdetails();
r.Display();
Console.ReadLine();
}
}
}

:class关键字用于声明一个类。

成员变量:变量是类的属性或数据成员,用于存储数据。在上述程序中,Rectangle 类有两个成员变量,名为length 和 width。

成员函数:函数是一系列执行指定任务的语句。类的成员函数是在类内声明的。在上述程序中,Rectangle 包含了三个成员函数:Acceptdetails,GetArea,和Display。

实例化一个类:上述程序中,类ExecuteRectangle 是一个包含 Main() 方法和实例化 Rectangle 类的类。

  • 标识符:用来识别类、变量、函数或者任何其他用户定义的项目。C#中,类的命名必须遵循如下基本规则:
    • 标识符必须以字母、下划线或 @ 开头,后面可以跟一系列的字母、数字、下划线和@;
    • 标识符第一个字符不能是数字;
    • 标识符必须不包含任何嵌入的空格或符号,比如?-+!#%^&*().;:"'/;
    • 标识符不能是C#关键字,除非它们由一个@前缀;
    • 标识符区分大小写;
    • 不能与C#的类库名称相同。

C#变量类型

  • 值 类型
    • 可以使用sizeof(变量类型) 获取类型的以字节为单位存储对象或类型的存储尺寸。
Console.WriteLine("sizeof(uint)={0}",sizeof(uint));
Console.WriteLine("sizeof(ulong)={0}", sizeof(ulong));
Console.WriteLine("sizeof(decimal)={0}",sizeof(decimal));
Console.WriteLine("sizeof(bool)={0}", sizeof(bool));
Console.WriteLine("sizeof(double)={0}", sizeof(double));

  • 引用 类型

    不包含存储在变量中的实际数据,而是包含对变量的引用,即它指的是一个内存的位置。使用多个变量时,引用类型可以指向一个内存位置,如果内存位置的数据是由一个变量改变的,其他变量会自动反映这种值的变化。内置引用类型有:object、dynamic 、string

    • 对象类型 object:是C#通用类型系统(Common Type System - CTS)中所有数据类型的中继基类。Object是System.Object 类的别名,所以对象(Object)类型可以被分配任何其他类型的值,但是在分配值前,需要继续类型转换。当一个值类型转换为对象类型时,被称为装箱,当一个对象类型转换为值类型时,被称为拆箱
    object obj;
    obj = 100; //装箱
    • 动态类型 dynamic :可以存储任何类型的值在动态数据类型变量中。对象类型变量的类型检查是在编译时发生的,而动态类型变量的类型检查是在运行时发生的。
    • 字符串类型 string :是System.String 类的别名,它是从对象类型派生的。字符串类型的值可以通过两种方式进行分配:引号@引号
    String str = "string_test";
    String str = @"C:\Windows";
    String str = "C:\\Windows"; //与上面语句等价

    string字符串前面可以加@(”逐字字符串“)将转义字符\当作普通字符对待。@字符串中可以任意换行,换行符以及缩进空格都计算在字符串长度之内。

    用户自定义引用类型有:class,interface,或delegate

  • 指针 类型:指针类型便改良存储另一种类型的内存地址,C#中的指针于C++或C中的指针具有相同的功能

type* identifier

类型转换

  • 隐式类型转换:将一个较小范围的数据类型转换为较大范围的数据类型,编译器自动完成类型转换,是C#默认的以安全方式进行的转换,不会导致数据丢失。

  • 显示类型转换:即强制类型转换;将一个较大范围的数据类型转换为较小范围的数据类型时,或者将一个对象类型转换为另一个对象类型时,需要使用强制类型转换符号进行希纳是转换,会造成数据丢失。

    C#提供了内置的类型转换方法ToBoolean(),ToByte(),ToChar(),ToDecimal(),ToDouble(),ToInt16(),ToInt32,ToInt64(),ToSbyte(),ToSingle(),ToString(),ToType(),ToUInt16(),ToUInt32(),ToUInt64()

double db = 3.141592635;
Console.WriteLine(db.ToString());

隐式类型转换只能小转大,不能大转小;

显示转换可能导致数据丢失、精度降低,需要进行数据类型的兼容性检查;

对象类型转换需要进行类型转换的兼容性检查和安全性检查。

变量

namespace cslearning
{
class Class1
{
static void Main(string[] args)
{
short a = 10;
int b = 20;
double c = a+b;
Console.WriteLine("a={0},b={1},c={2}", a, b, c);
}
}
}

接受来自用户的值

System命名空间中的Console类提供了一个函数 ReadLine(),用于接收来自用户的输入并把它存储到一个变量中。

int num;
num = Convert.ToInt32(Console.ReadLine()); //把用户输入的数据转换为int类型,因为Console.ReadLine()只接受字符串格式的数据。
Console.WriteLine(num);
String str = Console.ReadLine();
Console.WriteLine(str);

lvalue表达式可以出现在赋值语句的左边或右边

rvalue表达式只能出现在赋值语句的右边

常量:常量可以实任何基本数据类型:整数常量、浮点常量、字符常量、字符串常量、枚举常量。常量可以被当作常规的变量,只是它们的值在定义后不能被修改。

  • 整数常量:可以为十进制、八进制、十六进制的常量,0x 或 0X表示十六进制,0 表示八进制,没有前缀表示十进制。整数常量也可以有后缀,可以是U和L的组合,U表示unsignedL表示long;后缀可以实大写或小写,多个后缀以任意顺序进行组合。
85 //十进制
0123 //八进制
0x4b //十六进制
30 //int
30u //无符号int
30l //long
30ul //无符号long
  • 浮点常量:浮点常量由整数部分、小数点、小数部分、指数部分组成,可以使用小数形式或指数形式来表示浮点常量。使用浮点形式表示时,必须包含小数点、指数或者同时包含两者,使用指数形式表示时,必须包含整数部分、小数部分或同时包含两。有符号的指数是用e/E表示的。

  • 字符常量:包括普通字符、转义序列、通用字符,括在单引号中。

  • 字符串常量:包括普通字符、转义序列、通用字符,括在双引号"",或则括在@""

定义常量:使用const关键字来定义

namespace cslearning
{
public class ConstTest
{
class SampleClass
{
public int x;
public int y;
public const int c1 = 5;
public const int c2 = c1 + 5;
public SampleClass(int p1, int p2)
{
x = p1;
y = p2;
}
}
static void Main(string[] args)
{
SampleClass mC = new SampleClass(11, 22);
Console.WriteLine("x={0},y={1}",mC.x,mC.y);
Console.WriteLine("c1={0},c2={1}",SampleClass.c1,SampleClass.c2);
}
}
}
/*
输出:
x=11,y=22
c1=5,c2=10
*/

运算符:

  • 算术运算符:+ - * / % ++ --
namespace OperatorApp1
{
class Program
{
static void Main(string[] args)
{
int a = 21;
int b = 10;
int c;
c = a + b;
Console.WriteLine("Line1 - c 的值是{0}",c);
c = a - b;
Console.WriteLine("Line2 - c 的值是{0}", c);
c = a * b;
Console.WriteLine("Line3 - c 的值是{0}", c);
c = a / b;
Console.WriteLine("Line4 - c 的值是{0}", c);
c = a % b;
Console.WriteLine("Line5 - c 的值是{0}", c);
c = ++a;
Console.WriteLine("Line6 - c 的值是{0}", c);
c = --a;
Console.WriteLine("Line7 - c 的值是{0}", c);
}
}
}
  • 关系运算符:== != > < <= >=
  • 逻辑运算符:&& || !
  • 位运算符:& | ^ ~ << >>
  • 赋值运算符:= += -= *= /= %= <<= >>= &= ^= |=
  • 其他运算符:
    • sizeof():返回数据大小
    • typeof():返回class类型
    • &:返回变量的地址
    • *:返回变量的指针
    • ?: :条件表达式
    • is:判断对象是否为某一类型
    • as:强制转换,机试转换失败也不会抛出异常

判断

  • if else
  • switch语句
    • switch 语句中的 expression 必须是一个整型或枚举类型,或者是一个class类型,其中class有一个单一的转换函数将其转换为整型或枚举类型。
    • 在一个switch 中可以有任意数量的case语句,每个case后跟一个要比较的值和一个冒号。
    • case 的 constant-expression 必须与 switch 中的变量具有相同的数据类型,且必须是一个常量。
    • 当被测试的变量等于 case 中的常量时,case后跟的语句将被执行,知道遇到break 语句为止。
    • 当遇到 break 语句时,switch 终止,控制流将跳转到 switch 语句后的下一行。
    • 不是每一个 case 都需要包含 break ,如果case语句为空,则可以不包含 break ,控制流将会 继续 后续的case,知道遇到break为止。
    • C# 不允许从一个case 部分继续执行到下一个case 部分。如果case 语句中有已经执行,则必须包含 break 或其他跳转语句。
    • 一个switch 语句可以有一个可选的 default 语句,在 switch 的结尾,default 语句用于在上面所有 case 都不为true是执行的一个任务。 default 也要包含break 语句,这是一个良好的习惯。
    • C#不支持从一个case 标签显示贯穿到另一个 case 标签,如果要使 C# 支持从一个case 标签显示贯穿到另一个case标签,可以使用goto 一个switch-case 或goto default。

循环

  • while

  • for/foreach

    • for循环
    for(init; condition; increment)
    {
    statement(s);
    }
    /*
    1、init 会首先被执行,且只会执行一次。这一步允许程序员声明并初始化任何循环控制变量,也可以不在这里写任何语句,只要有一个分号出现即可。
    2、判断 condition 。如果为真,则执行循环主体。如果为假,则不执行循环主体,且控制流会跳转到紧邻着for循环的下一条语句。
    3、执行完for循环主体后,控制流会跳回上面的increment 语句,该语句允许程序员更新循环控制变量。该语句可以留空,只要在条件后有一个分号出现即可。
    4、条件再次被判断。如果为真,则执行循环,这个过程会不断重复(循环主体,然后增加步值,再然后重新判断条件)。在条件变为假时,for循环终止。
    */
    • foreach:foreach可以迭代数组或者一个集合对象。foreach循环可以用来遍历集合类型,如数组、列表、字典等,它是一个简化版的for循环,使得代码更加简洁易读。
    foreach(var item in collection)
    {
    //循环
    }
    //collection 是要遍历的集合,item 是当前遍历到的元素。
    class ForEachTest
    {
    static void Main(string[] args)
    {
    int[] fibarray = new int[] { 0, 1, 1, 2, 3, 5, 8, 13 };
    foreach (int element in fibarray)
    {
    Console.WriteLine(element);
    }
    for (int i = 0; i < fibarray.Length; i++)
    {
    Console.WriteLine(fibarray[i]);
    }
    int count = 0;
    foreach (int element in fibarray)
    {
    count += 1;
    Console.WriteLine("Element #{0}:{1}", count, element);
    }
    Console.WriteLine("Number of element in the array :{0}", count);
    }
    }
    class Program
    {
    static void Main(string[] args)
    {
    //创建一个字符串列表
    List<string> myString = new List<string>();
    //向列表添加一些字符串元素
    myString.Add("Google");
    myString.Add("Meta");
    myString.Add("Amazon");
    //使用foreach 循环遍历列表中的元素
    foreach(string s in myString)
    {
    Console.WriteLine(s);
    }
    }
    }
  • do while

  • 嵌套

循环控制语句:

  • break 语句:终止loop或switch 语句,程序流将继续执行紧接着loop或switch 的下一条语句
    • 当break 语句出现在一个循环内时,循环会立即终止,且程序流将继续执行紧接着循环的下一条语句。
    • 用于终止switch语句中的一个case
    • 若使用嵌套循环,break 语句会立即停止执行最内层的循环,然后开始执行该块之后的下一行代码。
  • continue 语句:跳过本轮循环,开始下一轮循环

无限循环:如果条件永远不为假,循环将变成无限循环。for(;😉 当表达式不存在时它被假设为真,可用于表示一个无限循环。

封装:封装被定义为“把一个或多个项目封闭在一个物理的或逻辑的包中”。在面向对象程序设计方法论中,封装是防止对实现细节的访问。

抽象和封装是面向对象程序设计的相关特性,抽象允许相关信息可视化,封装则使开发者实现所需级别的抽象。

C#封装根据具体的需要,设置使用者的访问权限,并通过访问修饰符 来实现。一个访问修饰符 定义了一个类成员的范围和可见性。C#支持的访问修饰符有:

  • public :所有对象都可以访问;允许一个类将其成员变量和成员函数暴露给其他的函数和对象,任何公有成员都可以被外部的类访问。

    namespace RectangleApplication
    {
    class Rectangle
    {
    //length 和 width 都被声明为public ,所以它们可以被函数Main() 和Rectangle类的实例r访问
    //成员函数 GetArea() 和 Display() 可以直接访问这些变量
    public double length;
    public double width;
    public double GetArea()
    {
    return length * width;
    }
    //成员函数Display()也被声明为public,所以它也能被Main() 使用Rectangle 类的实例 r访问
    public void Display()
    {
    Console.WriteLine("长度:{0}", length);
    Console.WriteLine("宽度:{0}", width);
    Console.WriteLine("面积:{0}", GetArea());
    }
    }
    class ExecuteRectangle
    {
    static void Main(string[] args)
    {
    Rectangle r = new Rectangle();
    r.length = 4;
    r.width = 3;
    r.Display();
    }
    }
    }
  • private:对象本身在对象内部可以访问;允许一个类将其成员变量和成员函数对其他的函数和对象进行隐藏,只有同一个类中的函数可以访问它的私有成员,即使是类的实例也不能访问它的私有成员。

    namespace RectangleApplication2
    {
    class Rectangle
    {
    //被声明为private,所以不能被Main()函数访问
    //成员函数Acceptdetails() 和 Display() 可以访问这些变量
    private double length;
    private double width;
    public void Acceptdetails()
    {
    Console.WriteLine("Please input length:");
    length= Convert.ToDouble(Console.ReadLine());
    Console.WriteLine("Please input width:");
    width= Convert.ToDouble(Console.ReadLine());
    }
    public double GetArea()
    {
    return length * width;
    }
    public void Display()
    {
    Console.WriteLine("Length:{0}", length);
    Console.WriteLine("Width:{0}", width);
    Console.WriteLine("Area:{0}", GetArea());
    }
    }
    class ExecuteRectangle
    {
    static void Main(string[] args)
    {
    Rectangle r = new Rectangle();
    //由于成员函数Acceptdetails() 和 Display() 被声明为public,所以它们可以被Main() 使用 Rectangle 类的实例r访问
    r.Acceptdetails();
    r.Display();
    }
    }
    }
  • protected:只有该类对象及其子类对象可以访问;即允许子类访问它的基类的成员变量和成员函数,有助于实现继承。

  • internal:同一个程序集的对象可以访问;即允许一个类将其成员变量和成员函数暴露给当前程序中的其他函数和对象。换句话说,带有internal访问修饰符的任何成员都可以被定义在该成员所定义的应用程序内的任何类或方法访问

    namespace RectangleApplication3
    {
    class Rectangle
    {
    internal double length;
    internal double width;
    //成员函数GetArea()声明是未带有任何访问修饰符,如果没有指定访问修饰符则使用类成员的默认访问修饰符 private
    double GetArea()
    {
    return length * width;
    }
    public void Display()
    {
    Console.WriteLine("Length:{0}", length);
    Console.WriteLine("Width:{0}", width);
    Console.WriteLine("Area:{0}",GetArea());
    }
    }
    class ExecuteRectangle
    {
    static void Main(string[] args)
    {
    Rectangle r = new Rectangle();
    r.length = 4;
    r.width = 3;
    r.Display();
    }
    }
    }
  • protected internal:访问限于当前程序集或派生自包含类的类。用于实现继承。

方法:一个方法是把一些相关的语句组织在一起,用来执行一个任务的语句块,每一个C#程序至少有一个带有Main方法的类。要使用方法,需要两步:定义方法+调用方法。

  • 定义方法

    <Access Specifier> <Return Type> <Method Name>(Parameter List)
    {
    Method Body
    }
    /*
    Access Specifier :访问修饰符,决定了变量或方法对于另一个类的可见性。
    Return Type:返回类型,一个方法可以返回一个值。返回类型是方法返回的值的数据类型。如果方法不返回任何值,则返回类型未void。
    Method Name:方法名称,是一个唯一的标识符,且大小写敏感,它不能与类中声明的其他标识符相同。
    Parameter List:参数列表,使用圆括号括起来,该参数是用来传递和接受方法的数据,参数列表是指方法的参数类型、顺序和数量。参数是可选的,即一个方法可能不包含参数。
    Method Body:方法体,包含了完成任务所需的指令集。
    */
  • 调用方法

    namespace CalculatorApplication
    {
    class NumberManipulator
    {
    public int FindMax(int num1,int num2)
    {
    return num1>num2 ? num1:num2;
    }
    static void Main(string[] args)
    {
    int a = 100, b = 200, ret;
    NumberManipulator n=new NumberManipulator();
    ret= n.FindMax(a,b);
    Console.WriteLine("The max number is {0}.",ret);
    }
    }
    }
    //亦可使用类的实例从另一个类中调用其他类的共有方法。
    //FindMax 属于 NumberManipulator类,可以从另一个类Test中调用它。
    namespace CalculatorApplication
    {
    class NumberManipulator
    {
    public int FindMax(int num1, int num2)
    {
    return num1>num2 ? num1 : num2;
    }
    }
    class Test
    {
    static void Main(string[] args)
    {
    int num1 = 100, num2 = 200, ret;
    NumberManipulator numberManipulator = new NumberManipulator();
    ret=numberManipulator.FindMax(num1, num2);
    Console.WriteLine("The max number is {0}.",ret);
    }
    }
    }
  • 递归方法调用:自我调用

    namespace CalculatorApplication
    {
    class NumberManupilator
    {
    public int factorial(int num)
    {
    if (num == 1) return 1;
    else return factorial(num - 1)*num;
    }
    static void Main(string[] args)
    {
    NumberManupilator numberManupilator = new NumberManupilator();
    Console.WriteLine("6 的阶乘是:{0}",numberManupilator.factorial(6));
    }
    }
    }
  • 参数传递

    • 值传递:复制参数的实际值给函数的形式参数,实参和形参使用的是两个不同内存中的值。在此情况下,当形参发生变化时,不会影响实参的值,从而保证了实参数据的安全。
    • 引用参数:复制参数的内存位置的引用给形式参数,当形参的值发生变化,同时实参的值也会改变。
    • 输出参数:可以返回多个值。

​ 按值传递参数(默认参数传递方式)。在此情况下,当调用一个方法时,会为每个值参数创建一个新的存储位置。实际参数的值会赋值给形参,实参和形参使用的是两个不同内存中的值。

namespace CalculatorApplication
{
class NumberManipulator
{
public void swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
static void Main(string[] args)
{
NumberManipulator numberManipulator = new NumberManipulator();
int a = 123, b = 321;
Console.WriteLine("Before swap,a = {0},b={1}", a,b);
numberManipulator.swap(a,b);
Console.WriteLine("After swap,a={0},b={1}", a, b);
}
}
}

​ 按引用传递参数,引用参数是一个对变量的内存位置的引用。当按引用传递参数时,它不会未这些参数创建一个新的存储位置,引用参数表示与提供给方法的实际参数具有相同的内存位置。C#使用 ref 关键字声明引用参数。

namespace CalculatorApplication
{
class NumberManipulator
{
public void swap(ref int x,ref int y)
{
int temp=x; x=y; y=temp;
}
static void Main(string[] args)
{
NumberManipulator numberManipulator = new NumberManipulator();
int x = 999, y = 888;
Console.WriteLine("Before swap,a = {0},b = {1}", x, y);
numberManipulator.swap(ref x, ref y);
Console.WriteLine("After swap,a = {0},b = {1}", x, y);
}
}
}

​ 按输出传递参数:return语句可用于只从函数中返回一个值。但是,可以使用 输出参数 来从函数中返回两个值。输出参数会把方法输出的数据赋给自己,其他方面与引用参数相似。C#使用 out 关键字声明输出传递参数。

namespace CalculatorApplication
{
class NumberManipulator
{
public void getValue(out int x)
{
int temp = 5;
x= temp;
}
static void Main(string[] args)
{
NumberManipulator numberManipulator = new NumberManipulator();
int x = 199;
Console.WriteLine("Before invoke x = {0}", x);
numberManipulator.getValue(out x);
Console.WriteLine("After invoke x = {0}", x);
}
}
}
/*
Before invoke x = 199
After invoke x = 5
*/
//提供给输出参数的变量不需要赋值,当需要从一个参数没有指定初值的方法中返回值时,输出参数非常有用。
namespace CalculatorApplication
{
class NumberManipulator
{
public void getValues(out int x,out int y)
{
Console.WriteLine("Please input the first number:");
x=Convert.ToInt32(Console.ReadLine());
Console.WriteLine("Please input the second number:");
y=Convert.ToInt32(Console.ReadLine());
}
static void Main(string[] args)
{
NumberManipulator numberManipulator = new NumberManipulator();
int x, y;
numberManipulator.getValues(out x, out y);
Console.WriteLine("After invoke x = {0},y = {1}", x, y);
}
}
}
/*
Please input the first number:
334
Please input the second number:
301
After invoke x = 334,y = 301
*/

可空类型

  • ?单问号用于对int、double、bool等无法直接赋值为null的数据类型进行null的赋值,意思是这个数据类型是Nullable 类型的。

  • ??双问号用于判断一个变量在为null的时候返回一个指定的值。

  • 可空类型 Nullable :一种个数的数据类型nullable类型,可空类型可表示其基础值类型正常范围内的值+null

    Nullable,读作”可空的int32“。

    处理数据库和其他包含可能未赋值的元素的数据类型时,将null赋值给数值类型或布尔类型的功能特别有用。例如数据库中的布尔型字段可以存储值true或false 或者该字段也可以未定义。

    <data_type>? <variable_name> = null

    namespace CalculatorApplication
    {
    class NullableAtShow
    {
    static void Main(string[] args)
    {
    int? x = null;
    int? y = 999;
    double? z = new double?();
    double? w = 3.1415926535;
    bool? boolval = new bool?();
    Console.WriteLine("Show nullable values :{0},{1},{2},{3}",x,y,z,w);
    Console.WriteLine("Show nullable boolean value :{0}", boolval);
    }
    }
    }
    /*
    Show nullable values :,999,,3.1415926535
    Show nullable boolean value :
    */
  • Null 合并运算符 ??

    Null合并运算符用于定义可空类型和引用类型的默认值。Null合并运算符为类型转换定义了一个预设值,以防可空类型的值为Null。Null合并运算符把操作数类型隐式转换为另一个可空(或不可空)的值类型的操作数的类型。如果第一个操作数的值为null,则运算符返回第二个操作数的值,否则返回第一个操作数的值。

    namespace CalculatorApplication
    {
    class NullablesAtShow
    {
    static void Main(string[] args)
    {
    double? x = null;
    double? y = 3.1111111;
    double? z;
    z = x ?? 342.243;//如果x的值为null则返回342.243
    Console.WriteLine("The value of z is {0}", z);
    z = y ?? 4;
    Console.WriteLine("The value of z is {0}", z);
    }
    }
    }
    /*
    The value of z is 342.243
    The value of z is 3.1111111
    */

数组

datatype[] arrayName;

​ datatype 用于指定被存储在数组中的元素的类型;

​ [] 指定数组的秩(维度);

​ arrayName 指定数组的名称。

初始化数组:声明一个数组不会在内存中初始化数组,当初始化数组变量时可以给数组赋值。数组是一个引用类型,需要使用new 关键字来创建数组的实例。

double[] db = new double[99];

可以通过赋值一个数组变量到另一个目标数组变量中,这样,目标和源都会指向相同的内存位置。

int[] x = new int[]{1,2,3};
int[] y = x;

当创建一个数组时,C#编译器会根据数组类型隐式初始化每个数组元素为一个默认值,例如int数组所有元素都会被初始化为0。

多维数组

string[,] names;
int[,,] x;
int y = x[2,3];

交错数组

int[][] scores;

声明一个数组不会在内存中创建数组。

传递数组给函数

参数数组:当声明一个方法时,不确定要传递给函数作为参数的参数数目,C#中使用参数数组用于传递未知数量的参数给函数。

  • params 关键字

Array类

  • 属性
    • IsFixedSize
    • IsReadOnly
    • Length
    • LongLength
    • Rank 获取数组的秩/维度
  • 方法
    • Clear
    • Copy(Array ,Array,Int32)
    • CopyTo(Array Int32)
    • GetLength
    • GetLongLength
    • GetLowerBound
    • GetUpperBound
    • GetType
    • GetValue(Int32)
    • IndexOf(Array,Object)
    • Reverse(Array)
    • SetValue(Object,Int32)
    • Sort(Array)
    • ToString

字符串

结构体

  • 特点:
    • 结构可带有方法、字段、索引、属性、运算符方法和事件;
    • 结构可定义构造函数,但不能定义析构函数。但是不能为结构定义无参构造函数。无参构造函数(默认)是自动定义的且不能被改变。
    • 结构体不能继承其他的结构或类。(不同于类)
    • 结构不能作为其他结构或类的基础结构。
    • 结构可实现一个或多个接口。
    • 结构成员不能指定为 abstract、virtual、protected
    • 当使用 new 操作符创建一个结构对象时,会调用适当的构造函数来创建结构。与类不同,结构可以不使用 new 操作符即可被实例化。
    • 如果不使用 new 操作符,只有在所有字段都被初始化之后字段才被赋值,对象才被使用。
  • 类vs结构
    • 类是引用类型,结构是值类型;
    • 结构不支持继承;
    • 结构不能声明默认的构造函数。

枚举是一组命名整型常量,使用 enum 关键字声明。C#枚举是值类型,枚举包含自己的值,且不能继承或传递继承。

枚举列表中的每个符号代表一个整数值,一个比它前的符号大的整数值,默认情况下,第一个枚举符号的值为0。

enum <enum_name>{
enumeration list
};
namespace EnumTest
{
public class EnumTest
{
enum Day { Sun,Mon,Tue,Wed,Thu,Fri,Sat};
static void Main()
{
int x = (int)Day.Sun;
int y = (int)Day.Mon;
Console.WriteLine("Sun = {0},Mon = {1}",x,y);
}
}
}

:当定义一个类时,实际并未定义任何数据,但定义了类的名称意味着类的对象由什么组成及在这个对象上可以执行什么操作。对象是类的实例,构成类的方法和变量称为类的成员。类的定义以关键字 class 开始,后面跟类的名称,类的主体在一对花括号内。

<access specifier> class clsaa_name
{
<access specifier> <data_type> variable1;
<access specifier> <data_type> variable2;
<access specifier> <return data_type> method1(parameters_list)
{
//method body
}
<access specifier> <return data_type> method2(parameters_list)
{
//method body
}
}
  • 注意:

    • 访问标识符指定了对类及其成员的访问规则,如果没有指定,则使用默认的访问标识符 internal,成员的默认访问标识符是 private
    • 数据类型指定了变量的类型,返回类型指定了返回的方法返回的数据类型
    • 如果要访问类的成员,使用点运算符
    • 点运算符链接了对象的名称和成员的名称
  • 成员函数和封装:

    • 类的成员函数是一个在类的定义中有它的定义或原型的函数,就像其他变量一样,作为类的一员,它能在类的任何对象上操作,且能访问该对象的类的所有成员。
    • 成员变量是对象的属性(从设计的角度),且它们保持私有来实现封装,这些变量只能用公共成员函数来访问。
  • 构造函数:是类的一个特殊的成员函数,当创建类的新的对象时执行。构造函数的名称与类的名称完全相同,它没有任何返回类型。

    默认的构造函数没有任何参数,但若需要一个带有参数的构造函数,可以有参数,这种构造函数叫做参数化构造函数,通过这种方法可在创建对象的同时给对象赋初始值。

namespace LineApplication
{
class Line
{
private double length;
public Line() {
Console.WriteLine("Object has been created!");
}
public void setLength(double length)
{
this.length = length;
}
public double getLength()
{
return length;
}
static void Main(string[] args)
{
Line line = new Line();
line.setLength(10);
Console.WriteLine("The length of line is {0}",line.getLength());
}
}
}
namespace LineApplication
{
class Line
{
private double length;
public Line(double len)
{
length = len;
Console.WriteLine("Object has been created,length = {0}", length);
}
public void setLength(double len)
{
this.length = len;
}
public double getLength()
{
return length;
}
static void Main(string[] args)
{
Line line = new Line(345);
Console.WriteLine("The length of line is {0}.",line.getLength());
line.setLength(90);
Console.WriteLine("The length of line now is {0}.",line.getLength() );
}
}
}
  • 析构函数:是类的一个特殊的成员函数,当类的对象超出范围时执行。

    析构函数的名称时在类的名称前加上波浪形~ 作为前缀,不返回值,也不带任何参数。

    析构函数用于在结束程序(比如关闭文件、释放内存)之前释放资源。

    析构函数不能继承或重载。

    namespace LineApplication
    {
    class Line
    {
    private double length;
    public Line()
    {
    Console.WriteLine("Object has been created!");
    }
    //析构函数
    ~Line()
    {
    Console.WriteLine("Object has been deleted!");
    }
    public void setLength(double length)
    {
    this.length = length;
    }
    public double getLength()
    {
    return length;
    }
    static void Main(string[] args)
    {
    Line line = new Line();
    line.setLength(10);
    Console.WriteLine("The length of line is {0}.",line.getLength());
    }
    }
    }
    //测试时未显示删除
  • 静态成员

    • 使用 static 关键字把类成员定义为静态的。当我们声明一个类成员为静态时,意味着无论有多少个类的对象被创建,只会有一个该静态成员的副本

    • 关键字 static 意味着类中只有一个该成员的实例。静态变量用于定义常量,因为它们的值可以通过调用类而不需要创建类的实例来获取。静态变量可在成员函数或类的定义外部进行初始化,也可在类的内部初始化静态变量。

      namespace StaticVarApplication
      {
      class StaticVar
      {
      public static int num;
      public void count()
      {
      num++;
      }
      public int getNum()
      {
      return num;
      }
      }
      class StaticTester
      {
      static void Main(string[] args)
      {
      StaticVar staticVar1 = new StaticVar();
      StaticVar staticVar2 = new StaticVar();
      for(int i = 0;i < 10; i++)
      {
      staticVar1.count();
      }
      for(int i = 0; i < 10; i++)
      {
      staticVar2.count();
      }
      Console.WriteLine("value of staticVar1 is {0}", staticVar1.getNum());
      Console.WriteLine("value of staticVar2 is {0}", staticVar2.getNum());
      }
      }
      }
      /*
      value of staticVar1 is 20
      value of staticVar2 is 20
      */
    • 也可使用 static成员函数 声明为静态函数,这样的函数只能访问静态变量。静态函数在对象被创建之前就已经存在。

      namespace StaticVarApplication
      {
      class StaticVar
      {
      public static int num;
      public void count()
      {
      num++;
      }
      public static int getNum()
      {
      return num;
      }
      }
      class StaticTester
      {
      static void Main(string[] args)
      {
      //静态函数在对象被创建出来前就已经存在
      Console.WriteLine("num = {0}", StaticVar.getNum());
      StaticVar staticVar = new StaticVar();
      staticVar.count();
      Console.WriteLine("num = {0}",StaticVar.getNum());
      }
      }
      }
      /*
      num = 0
      num = 1
      */

继承

  • 继承允许我们根据一个类来定义另一类,使得创建和维护应用程序变得更容易,同时也有利于重用代码和节省开发时间。当创建一个类时程序员无需重新编写新的数据成员和成员函数,只需要设计一个新的类,继承了已有的类的成员即可,这个已有的类被称为 基类 ,新的类被称为 派生类。继承的思想实现了 属于(IS-A)关系。

  • 基类和派生类:一个类可以派生自多个类或接口,这意味它可以从多个基类或接口继承数据和函数。

    <access specifier> class <基类>
    {
    ~~~~~
    }
    class <派生类> : <基类>
    {
    ~~~~~
    }
    namespace InherittanceApplication
    {
    class Shape
    {
    protected int width;
    protected int height;
    public void setWidth(int w)
    {
    width = w;
    }
    public void setHeight(int h)
    {
    height = h;
    }
    }
    class Rectangle : Shape
    {
    public int getArea()
    {
    return width * height;
    }
    }
    class RectangleTester
    {
    static void Main(string[] args)
    {
    Rectangle rectangle = new Rectangle();
    rectangle.setHeight(10);
    rectangle.setWidth(5);
    Console.WriteLine("Area is {0}",rectangle.getArea());
    }
    }
    }
  • 基类的初始化

    派生类继承了积累的成员变量和成员方法,因此父类对象应该在子类对象之前被创建。

    namespace RectangleApplication
    {
    class Rectangle
    {
    protected double length;
    protected double width;
    public Rectangle(double length, double width)
    {
    this.length = length;
    this.width = width;
    }
    public double GetArea()
    {
    return length * width;
    }
    public void Display()
    {
    Console.WriteLine("Length is {0}", length);
    Console.WriteLine("Width is {0}", width);
    Console.WriteLine("Area is {0}",GetArea());
    }
    }
    class Tabletop : Rectangle
    {
    private double cost;
    public Tabletop(double l,double w):base(l,w) { }
    public double GetCost()
    {
    return GetArea()*70;
    }
    public void Display()
    {
    base.Display();
    Console.WriteLine("Cost is {0}",GetCost());
    }
    }
    class ExecuteRectangle
    {
    static void Main(string[] args)
    {
    Tabletop tabletop = new Tabletop(4, 3);
    tabletop.Display();
    }
    }
    }
    /*
    Length is 4
    Width is 3
    Area is 12
    Cost is 840
    */
  • 多重继承

    多重继承是指一个类别可以同时从多于一个父类继承行为与特征的功能。与单一继承相对,单一继承是指一个类别只可以继承自一个父类。C#不支持多重继承,但是可以通过接口来实现多重继承。

    namespace InheritanceApplication
    {
    class Shape
    {
    protected int width, height;
    public void setWidth(int w)
    {
    width = w;
    }
    public void setHeight(int h)
    {
    height = h;
    }
    }
    //基类 PaintCost
    public interface PaintCost
    {
    int getCost(int area);
    }
    //派生类
    class Rectangle : Shape,PaintCost
    {
    public int getCost(int area)
    {
    return area*70;
    }
    public int getArea()
    {
    return width*height;
    }
    }
    class RectangleTester
    {
    static void Main(string[] args)
    {
    Rectangle Rect = new Rectangle();
    Rect.setWidth(10);
    Rect.setHeight(10);
    Console.WriteLine("Total area is {0}",Rect.getArea());
    Console.WriteLine("Total cost is {0}", Rect.getCost(Rect.getArea()));
    }
    }
    }

多态

  • 多态是同一个行为具有多个不同的表现形式或形态的能力。多态性意味着有多种形式。在面向对象编程范式中,多态性往往表现为”一个接口、多个功能“。多态性可为静态或动态。在静态多态性中,函数的响应是在编译时发生的;在动态多态性中,函数的响应是在运行时发生的。

    • 静态多态:在编译时,函数和对象的连接机制被称为早期绑定,也被称为静态绑定。

      • 函数重载:在同一个范围内对相同的函数名有多个定义,函数的定义必须彼此不同,可以实参数列表中的参数类型不同,也可以是参数个数不同。不能重载只有返回类型不同的函数声明。

        namespace PolymorphismApplication
        {
        public class TestData
        {
        public int Add(int a,int b,int c)
        {
        return a + b + c;
        }
        public int Add(int a,int b) { return a + b; }
        }
        class Program
        {
        static void Main(string[] args)
        {
        TestData data = new TestData();
        Console.WriteLine("add1 :" + data.Add(1, 2));
        Console.WriteLine("add2 :"+data.Add(1,2,3));
        }
        }
        }
      • 运算符重载

    • 动态多态:C#允许使用 abstract 创建抽象类,用于提供接口的部分类的实现,当一个派生类继承自该抽象类时,实现即完成。抽象类 包含抽象方法,抽象方法可被派生类实现。

      • 规则:

        • 不能创建一个抽象类的实例

        • 不能在一个抽象类外部声明一个抽象方法

        • 通过在类定义前面放置关键字 sealed 可以将类声明为密封类。当一个类被声明为sealed时,它不能被继承。抽象类不能被声明为sealed。

          namespace PolymorphismApplication
          {
          abstract class Shape
          {
          abstract public int area();
          }
          class Rectangle : Shape
          {
          private int length;
          private int width;
          public Rectangle(int a=0,int b = 0)
          {
          this.width = a;
          this.length = b;
          }
          public override int area()
          {
          Console.WriteLine("Rectangle 类的面积为:");
          return width * length;
          }
          }
          class RectangleTester
          {
          static void Main(string[] args)
          {
          Rectangle rectangle = new Rectangle(10,9);
          Console.WriteLine("面积为:{0}",rectangle.area());
          }
          }
          }
      • 当有一个定义在类中的函数需要在继承类中实现时,可以使用虚方法

        • 虚方法 使用 关键字 virtual 声明的

        • 虚方法可以在不同的继承类中有不同的实现

        • 对虚方法的调用是在运行时发生的

        • 动态多态性是通过 抽象类虚方法实现的

          namespace PolymorphismApplication
          {
          public class Shape
          {
          public int X { get; private set; }
          public int Y { get; private set; }
          public int Height { get; set; }
          public int Width { get; set; }
          public virtual void Draw()
          {
          Console.WriteLine("执行基类的画图任务");
          }
          }
          class Circle : Shape
          {
          public override void Draw()
          {
          Console.WriteLine("画一个圆形");
          base.Draw();
          }
          }
          class Rectangle : Shape
          {
          public override void Draw()
          {
          Console.WriteLine("画一个长方形");
          base.Draw();
          }
          }
          class Triangle : Shape
          {
          public override void Draw()
          {
          Console.WriteLine("画一个三角形");
          base.Draw();
          }
          }
          class Program
          {
          static void Main(string[] args)
          {
          //创建一个List<Shape>对象,并向该对象添加 Circle、Triangle和Rectangle
          var shapes = new List<Shape>
          {
          new Circle(), new Rectangle(), new Triangle()
          };
          //使用foreach 循环对该列表的派生类进行循环访问,并对其中的每个Shape对象调用Draw方法
          foreach (var shape in shapes)
          {
          shape.Draw();
          }
          }
          }
          }

运算符重载

  • 重载运算符是具有特殊名称的函数,通过关键字 operator 后跟运算符的符号来定义的,重载运算符有返回类型和参数列表。
namespace OperatorOvlApplication
{
class Box
{
private double length;
private double breadth;
private double height;
public double getVolume()
{
return length*height*breadth;
}
public void setLength(double length)
{
this.length = length;
}
public void setBreadth(double breadth)
{
this.breadth = breadth;
}
public void setHeight(double height)
{
this.height = height;
}
//重载 + 运算符来把两个 Box 对象相加
public static Box operator+(Box left, Box right)
{
Box box = new Box();
box.length =left.length+right.length;
box.breadth =left.breadth+right.breadth;
box.height =left.height+right.height;
return box;
}
}
class Tester
{
static void Main(string[] args)
{
Box box1 = new Box();
Box box2 = new Box();
Box box3 = new Box();
box1.setLength(10);
box1.setHeight(10);
box1.setBreadth(10);
box2.setLength(10);
box2.setHeight(10);
box2.setBreadth(10);
box3 = box1 + box2;
Console.WriteLine("The volumn of box1 is {0}",box1.getVolume());
Console.WriteLine("The volumn of box2 is {0}",box2.getVolume());
Console.WriteLine("The volumn of box3 is {0}",box3.getVolume());
}
}
}
  • 可重载运算符
    • 一元运算符:**+ - ! ~ ++ -- **
    • 二元运算符:+ - * / %
    • 比较运算符:== != < > <= >=
  • 不能被直接重载的运算符
    • 条件逻辑运算符:&& ||
  • 不可重载运算符
    • 赋值运算符:+= -= *= /= %=
    • = . ?: -> new is sizeof typeof

**接口 Interface **:

  • 接口定义了所有类继承接口时应遵循的语法合同,接口定义了语法合同是什么部分,派生类定义了语法合同怎么做部分。接口定义了属性、方法和事件,这些都是接口的成员,接口只包含了成员的声明,成员的定义是派生类的责任。接口提供了派生类应遵循的标准结构。

    接口使得实现接口的类或结构在形式上保持一致。抽象类某种程度上与接口类似,但是,它们大多只是用在当只有少数方法由基类声明由派生类实现时。接口本身并不实现任何功能,它只是和声明实现该接口的对象订立一个必须实现哪些行为的契约。抽象类不能直接实例化,但允许派生出具体的具有实际功能的类。

  • 使用 interface 关键字声明,默认是public

interface IMyInterface//通常接口命令以字母 I 开头
{
void MethodToImplement();
}
class InterfaceImplementer : IMyinterface{//实现了接口
}
  • 如果一个接口继承其他接口,那么实现类或结构就需要实现所有接口的成员。

命名空间 Namespace

  • 命名空间的设计目的是提供一种让一组名称与其他名称分隔开的方式。在一个命名空间中声明的类的名称与另一个命名空间中声明的相同的类的名称不冲突。

  • namespace namespace_name{
    }
  • using 关键字表明程序使用的是给定命名空间中的名称。例如在程序中使用System命名空间,其中定义了类Console

System.Console.WriteLine("hello")
//等价于
using System;
Console.WriteLine("hello");
//使用using命名空间指令,这样在使用时就不用在前面加上命名空间名称,该指令告诉编译器随后的代码使用了指定命名空间中的名称。
  • 嵌套命名空间:可以在一个命名空间中定义另一个命名空间。可使用点运算符访问嵌套的命名空间成员。
namespace namespace_name1{
namespace namespace_name2{
}
}

预处理器指令

  • 预处理器指令指在实际编译开始之前对信息进行预处理。
  • 所有预处理器指令都以 # 开头,且在一行上,只有空白字符可以出现在预处理器指令之前。预处理器指令不是语句,所以不以分号结束。
  • C#编译器没有一个单独的预处理器,但是指令被处理时就像是有一个单独的预处理器一样。在C#中,预处理器指令用于在条件编译中起作用。一个预处理器指令必须是该行上的唯一指令。

正则表达式

异常处理

  • 异常提供一种把程序控制权从某个部分转移到另一个部分的方式,C#异常处理是建立在四个关键词上的:

    • try:一个try块标识了一个将被激活的特定的异常的代码块,后跟一个或多个 catch 块
    • catch:程序通过异常处理程序捕获异常,catch 关键字表示异常的捕获
    • finally:finally 块用于执行给定的语句,不管异常是否被抛出都会执行。例如打开一个文件,不管是否出现异常文件都要被关闭。
    • throw:当出现问题时,程序抛出一个异常,使用 throw 关键字来完成。
  • 语法:假设一个块将出现异常,一个方法是用try和catch关键字捕获异常。try/catch 块内的代码为受保护的代码。

    try
    {
    //引起异常的语句
    }
    catch(ExceptionName e1)
    {
    //错误处理代码
    }
    catch(ExceptionName e2)
    {
    //错误处理代码
    //可以列出多个catch语句捕获不同类型的异常,以防try块在不同的情况下生成多个异常
    }
    finally
    {
    //要执行的语句
    }
  • 异常类:C#异常是使用类来表示的,C#中的异常主要是直接或间接的派生于 System.Exception类。System.Application.ExceptionSystem.SystemException类是派生于System.Exception类的异常类。

    • System.ApplicationException 类支持由应用程序生成的异常,所以程序员定义的异常都应派生自该类。

    • System.SystemException 类是所有预定义的系统异常的基类。

      异常类 描述
      System.IO.IOException 处理I/O错误
      System.IndexOutOfRangeException 处理当方法指向超出范围的数组索引时生成的错误
      System.ArrayTypeMismatchException 处理当数组类型不匹配时生成的错误
      System.NullReferenceException 处理当依从一个空对象时生成的错误
      System.DivideByZeroException 处理当除以零时生成的错误
      System.InvalidCastException 处理在类型转换期间生成的错误
      System.OutOfMemoryException 处理空闲内存不足生成的错误
      System.StackOverflowException 处理栈溢出生成的错误
  • 异常处理:C#以try和catch块的形式提供了一种结构化的异常处理方案,使用这些块,把核心程序语句与错误处理语句分离。

namespace ErrorHandlingApplication
{
class DivNumbers
{
int result;
DivNumbers()
{
result = 0;
}
public void division(int x,int y)
{
try
{
result = x / y;
}catch (DivideByZeroException e)
{
Console.WriteLine("Exception caught:{0}", e);
}
finally
{
Console.WriteLine("Result:{0}", result);
}
}
static void Main(string[] args)
{
DivNumbers divNumbers = new DivNumbers();
divNumbers.division(1, 0);
}
}
}
  • 创建用户自定义异常
namespace UserDefinedException
{
public class TempIsZeroException : ApplicationException
{
public TempIsZeroException(string message):base(message) { }
}
public class Temperature
{
int temperature = 0;
public void showTemp()
{
if (temperature == 0)
{
throw (new TempIsZeroException("Zero Temperature found"));
}
else
{
Console.WriteLine("Temperature:{0}", temperature);
}
}
}
class TestTemperature
{
static void Main(string[] args)
{
Temperature t = new Temperature();
try
{
t.showTemp();
}
catch(TempIsZeroException e)
{
Console.WriteLine ("TempIsZeroException:{0}",e.Message);
}
}
}
}
  • 抛出对象:如果异常是直接或间接派生自 System.Exception类,程序员可以抛出一个对象。使用throw语句来抛出当前的对象。

文件的输入与输出

一个文件是一个存储在磁盘中带有指定名称和目录路径的数据集合,当打开文件进行读写时,它变成一个流。从根本上说,流是通过通信路径传递的字节序列。有两个主要的流:输入流输出流。输入流用于从文件读取数据,输出流用于向文件写入数据。

  • I/O类:System.IO命名空间有各种不同的类,用于执行各种文件操作,如创建和删除文件、读取或写入文件,关闭文件等。
I/O类 描述
BinaryReader 从二进制流读取原始数据
BinaryWriter 以二进制格式写入原始数据
  • FileStream类:System.IO命名空间中的FileStream类有助于文件的读写和关闭,该类派生自抽象类Stream。
//需要创建一个FileStream 对象来创建一个新的文件,或打开一个已有的文件,创建FileStream 对象的语法:
FileStream <object_name> = new FileStream(<file_name>,
<FileMode Enumerator>,<FileAccess Enumerator>,<FileShare Enumerator>);
namespace FileIOApplication
{
class Program
{
static void Main(string[] args)
{
FileStream f=new FileStream("testfile.dat",FileMode.OpenOrCreate,FileAccess.ReadWrite);
for(int i=0; i < 100; i++)
{
f.WriteByte((byte) i);
}
f.Position = 0;
for(int i = 0; i < 100; i++)
{
Console.WriteLine(f.ReadByte() + " ");
}
f.Close();
}
}
}

高级文件操作


posted @   ben犇  阅读(29)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示