​大家好!我是未来村村长,就是那个“请你跟我这样做,我就跟你这样做!”的村长👨‍🌾!​

👩‍🌾“人生苦短,你用Python”,“Java内卷,我用C#”。

从Java到C#,不仅仅是语言使用的改变,更是我从理想到现实,从象牙塔到大熔炉的第一步。.NET是微软的一盘棋,而C#是我的棋子,只希望微软能下好这盘棋,而我能好好利用这个棋子。

注:本篇文章将全面地梳理C#相关知识,没学过相关的面向对象编程语言的读者可能读起来比较难懂,望海涵。


文章目录







一、Hello,C#🎯

1、工程结构

using System;
class Hello{
static void Main(){
Console.WriteLine("Hello,C#");
}
}

​ 第一行:开using System是导入System类,在C#中如果不导入类的话,需要通过​​类名.方法名(参数)​​进行调用。

第二行:类的声明,所谓类是面向对象的编程方式,万物皆对象,对象的归属定义便称为类。如哈士奇与狗的关系,便是对象与类的关系。

​ 第三行:同许Java语言和C语言一样,static void Main()是C#的入口类,其中void可以替换为其它返回类型。完整声明可写为​​static int Main(String args)​​。这里你会发现,该语言与其它语言在方法上的不同,Java一般会使用小写字母开头作为方法的标识符,而C#会使用大写字母开头作为方法标识符。

第四行:语句,如同Java中的println()方法的使用。

2、输入与输出

(1)基础用法

class IO{
static void Main(){
System.Console.WriteLine("自动换行的输出");
System.Console.Write("不会自动换行的输出\n");
string line = System.Console.ReadLine();//读取一行
int oneChar = System.Console.Read();//读取一个字符
}
}

WriteLine和Write是输出,ReadLine和Read是输入。其中Read方法返回的值为int类型,ReadLine返回stirng类型。

(2)格式化输出

① 字符串插值:在字符串前加上$表示使用字符串插值

string key = "value";
System.Console.WriteLine($"输出key:{key}");

② 复合格式化:以下0和1表示占位符,0指代第一个字符串变量person,1指代i

string i = "I";
string person = "person";
System.Console.WriteLine("{1} am a {0}",person,i);

二、数据类型🧩

1、整数类型

class X{
static void Main(){
//有符号
byte x2;//BCL名称:BYTE
short x3;//BCL名称:Int16
int x5;//BCL名称:Int32
long x7;//BCL名称:Int64,带后缀L或l
//无符号
sbyte x1;//BCL名称:SBYTE
ushort x4;//BCL名称:UInt16
uint x6;//BCL名称:UInt32,带后缀U或u
ulong x8;//BCL名称:UInt64,带后缀UL或ul
}
}

2、浮点类型

class X{
static void Main(){
float f = 1.1F;//BCL名称System.Single,后缀F或f
Double d = 1.1;//BCL名称System.single,后缀D或d,可无
}
}

float有效位数为7位,double有效位数为15~16位。

3、decimal类型

class X{
static void Main(){
decimal m = 1.111111m;//BCL名称System.Decimal,后缀m或M
}
}

decimal类型的有效位数位28~29位,float和double字面值的精度会和实际值存在微小的差异,不能做到完全准确,要避免使用二进制浮点数作为条件布尔表达式。decimal适用于财会,金融计算。

4、char类型

class X{
static void Main(){
char c = 'c';//BCL名称:System.Char
}
}

char的大小和uhsort大小相同,取值范围对应Unicode字符集。转义字符为char类型,常用的有\\,\n,\t,\"。

5、字符串类型

(1)string

class X{
static void Main(){
string s = "string";//BCL名称System.String
}
}

(2)字符串操作

① 字符串插值原理:$利用了string.Fotmat()方法的语法糖

string key = "value";
System.Console.WriteLine($"{key}");

//等同
object[] args = new object[]{key};
System.Console.WriteLine(string.Format("{0}",args));

② string静态方法

string.Format(string format,...);
//字符串连接,相等于+
string.Concat(string str0,string str1);
//字符串大小比较:相等为0,大于为正,小于为负
string.Compare(string str0,string,str1);

③ string实例方法

string str;
//判断开头或结尾字符,返回bool
str.StartsWith(string value);
str.EndsWith(string value);
//大小写转换
str.ToLower();
str.ToUpper;
//删除首尾空白
str.Trim();
str.TrimEnd();
str.TrimStart;
//字符串替换
str.Replace(string oldValue,string newValue);
//字符串长度
str.Length;

(3)字符串不可变

字符串不可变,每次对字符串的更改都是创建一个新的常量,没有被引用的字符串常量会被GC(垃圾回收)。我们可以使用System.Text.StringBuilder类型来代替string,StringBuilder包含Append()、AppendFormat()、Insert()、Remove()、Replace()等方法,这些方法会在StringBuilder本身的实例对象基础上去修改。

6、匿名类型

class X{
static void Main(){
var v = "nameless";
}
}

匿名类型是在方法内部动态声明的数据类型,可由编译器去判断变量的具体类型。

7、元组类型

class X{
static void Main(){
//形式1
(string key1,string key2) = ("value1","value2");
//形式2
string key1;
string key2;
(key1,key2) = ("value1","value2");
//具名元组
(string key1,string key2) y = ("value1","value2");
System.Console.WriteLine($"{y.key1},{y.key2}");
//匿名元组
var y = ("value1","value2");
System.console.WriteLine($"{y.item1},{y.item2}");
}
}

8、数组类型

(1)一维数组

class X{
static void Main(){
//声明时赋值
int[] a = {1,2,3};
int[] b = new int[]{1,2,3};
int[] c = new int[3]{1,2,3};
//先声明后赋值
int[] d;
d = new int[]{1,2,3};
}
}

分配数组时不指定初始值,会把数组中的每个元素初始化为其默认值。

(2)多维数组

class X{
static void Main(){
//声明二维数组或三维数组
int[,] a = new int[2,1];
int[,,] c = new int[2,2,3];
//声明时初始化
int[,] b = {{0},{1}};
}
}

(3)交错数组

class x{
static void Main(){
int[][] a = {new int[]{1,2,3},
new int[]{1,2},
new int[]{1,2,3,4,5}};
}
}

(4)数组的使用

class x{
static void Main(){
string[] str = {"1","2"};
//数组属性
string str1 = str[0];
int length = str1.Length;
//操作数组的方法—借助Array
System.Array.Sort(str);//排序
System.Array.Reverse(str);//逆转
System.Array.Clear(str);//设为默认值
//字符串转为char数组
char[] c = str.ToCharArray();
}
}

注意:

  • 声明数组的[]必须放在类型之后,不能放在其它地方
  • 声明之后再进行赋值必须使用new关键字
  • 若未提供直接对数组进行赋值,则必须要指定数组的大小

9、结构类型

(1)定义

结构类型是指把各种不同类型数据信息组合在一起形成的组合类型,结构是用户自定义的数据类型。采用关键字struct进行创建,结构体中可以定义相应的属性和方法。

struct Student{
long no;
string name;
int age;
void Method(){}
}

(2)成员访问

Student student;
student.name;
student.Method();

三、操作符与控制流🥏

1、操作符

  • 运算:+、-、*、/、%
  • 赋值:=、+=、-=、*=、/=、%=
  • 递增/减:++、–

需要注意的是浮点类型的运算精度会出现不准确的情况,char类型字符可视为ushort进行整数运算操作。

2、布尔表达式

class Y{
static void Main(){
bool t = true;
bool f = false;
//逻辑操作符
bool r = t || f;
bool r = t && f;
bool r = !f;
//条件操作符
//三元操作符
int a = 0;
int b = 1;
int c = r ? a : b;
//空合并操作符
object d = null;
int e = d ?? c;//如果d为null,则e为c
//空条件操作符
int[] arr;
int a = arr?.Length;//在空值中通过?.调用实例方法则会报错

}
}

2、控制流

(1)if-else if-else

if(xxx){}
else if(xxx){ }
else{}

(2)while和dowhile

while(){};
do{}while();

(3)for和foreach

for(int i = 0;i<10;i++){};
for(char i in strArray){
System.Console.Write(i);
}

(4)switch

switch(input){
case key:
case key:
case key:
default:
}

(5)break与continue

break:退出当前循环[放弃整个循环],continue:退出该层循环[放弃剩余语句,进入下一次循环]。

(6)goto

一般会避免使用goto

四、类与对象🏓

类是对象的抽象,对象是类的实例。一般通过[new 构造器]来进行对象的实例化,通过实例化对象去修改属性或调用方法。

ClassName objectName = new ClassName();
objectName.field;
objectName.Method();

1、类

(1)类的声明

[类修饰符] class 类名[:父类]{

}

修饰符

说明

new

新建类,表示隐藏由基类继承而来的与基类同名的成员,new修饰类只允许在嵌套类中表示类声明使用。

public

公有类,表示外界可以不受限制地访问。

protected

保护类,表示该类或从该类派生的类可以访问

internal

内部类,表示本程序的类可以访问

private

私有类,表示只有该类可以访问,无修饰符,默认为private修饰。

abstract

抽象类,说明该类是一个不完整的类,abstract修饰的类为抽象类,只能作为其他类的父类,不能直接使用。

sealed

密封类,说明该类不能作为其它类的基类,不能再派生新的类,sealed修饰的类为密封类,不能作为其它类的父类,可以继承其它类。abstract和sealed不能同时使用。

类的修饰符如上有多个,可以组合使用,其中public、protected、internal、private为访问修饰符。修饰类以后,不仅表明了类的访问权限,还表明了类中成员的访问权限

(2)类的成员

C#中一个类可包含以下成员:常量、变量、方法、属性、事件、索引器、运算符、构造函数、析构函数。

类的成员同样需要设定访问修饰符。

访问修饰符

说明

public

允许类的内部或外界直接访问。

private

只允许类内部访问,不允许外界和派生类访问。

protected

不允许外界访问,但允许派生类访问。

internal

允许同一个命名空间的类访问

readonly

该成员的值只能读,不能写,即除赋初始值以外,不能进行任何修改

(3)静态成员与实例成员

声明静态成员只需要加static,静态成员归类所有,可通过类名.静态成员名进行直接访问。

(4)构造函数与析构函数

构造函数是当类实例化时先执行的函数,析构函数是实例对象从内存中销毁时执行的函数。

class Point{
int x,y;
public Point(int x,int y){
this.x = x;
this.y = y;
}
}

构造函数的名字一定与类名相同,没有返回值,可以带参数,在对象定义时自动被调用,如果没有定义构造函数,编译器会自动生成无参构造。this一般用在构造函数中,区别同名的构造函数参数和类成员变量。

class Point{
~Point(){
System.Console.WriteLine("挥一挥衣袖,不带走一片云彩");
}
}

析构函数不能带参数和返回值,与类名相同但需要在函数名前加~,不能被继承和重载。

(5)索引指示器

使用索引指示器前需要先进行定义。

[修饰符] 类型名 this[参数列表]{
get{可执行语句}
set{可执行语句}
}

在set方法中,可以使用具有特殊隐含参数value,用以表达用户指定的值,而get方法使用return返回所得到的值。例如

class MyIndexer{
//属性
private string[] myArray = new string[4];
//索引指示器
private string this[int index]{
get{
if(index<0||index>=4) return null;
else return myArray[index];
}
set{
if(!(index<0||index>=4)) myArray[index] = value;
}
}
}

使用索引指示器访问对象。

class Test{
static void Main(){
MyIndexer idx = new MyIndexer();
idx[0] = "v";
idx[1] = "M";
}
}

2、委托和事件

(1)委托

//声明一个委托
delegate int MyDelegate();
//定义需要被代理的类
class Myclass{
//定义被代理方法
public int M1(){
System.WriteLine("");
return 0;
}
public static int M2(){
System.WriteLine("");
return 0;
}
}
//Main类
class Test{
static void Main(){
//定义被代理对象
Myclass w = new MyClass();
//实例化代理对象,传入相关方法
MyDelegate p = new MyDelegate(w.M1);
p();//通过代理对象(),进行代理方法的执行
//可直接代理静态方法
MyDelegate b = new MyDelegate(Mycalss.M2);
b();
//可通过+,+=,-,-=对代理对象进行组合执行
Mydelegate g = p + b;
g()

}
}

在C#中使用委托具体步骤如下:

  • 声明一个委托,其参数形式一定要和想要包含的方法的参数形式一致;
  • 创建委托对象并将所希望的方法包含在该委托对象中;
  • 通过委托对象调用包含在其中的各个方法。

(2)事件

//定义事件的委托
delegate void MyEventHandler();
class MyEvent{
//事件的声明
public event MyEventHandler activate;
//触发事件的方法
public void fire(){
if(activite!=null)
//事件的发生
activate();
}
}
class Test{
static void handler(){
System.Console.WriteLine("事件发生");
}
static void Main(){
MyEvent evt = new MyEvent();
//事件的预定
evt.activate += new MyEventHandler(handler);
evt.fire();
}
}

事件的创建与执行流程可分为:事件的声明、事件的预定和取消、事件的发生。

事件的声明:

[修饰符] event 委托名 事件名;

事件的预定和取消:

事件名 += new 委托名(方法名);
事件名 -= new 委托名(方法名);

事件的发生:

事件名(参数);

3、常用类的使用

(1)Math

class MathMain{
static void Main(){
Math.Abs();//返回指定数的绝对值
Math.Max();//返回两数最大
Math.Min();//返回两数最小
Math.Sqrt();//返回指定数开方
Math.Round();//按指定小数位数舍入
Math.Exp();//返回指定的指数
}
}

(2)Random

class RandomMain{
static void Main(){
Random.Next();//返回非负随机数
Random.Next(int n);//返回小于n的随机数
Random.Next(int m,int n);//返回范围在m到n的随机数
Random.NextDouble();//返回[0,1]之间的浮点数
}
}

(3)string

方法

说明

str1.Compare(string str2)

用于判断两个字符串是否相等,相等返回0,大于返回1,小于返回-1。也可使用==进行两个字符串的比较。

str.Insert(int index,string str)

在index处插入str,可使用+拼接两个字符串

str.Replace(string str1, string str2)

将str中的str1换成str2

str.Substring(int index,int n)

从index处取str的n个字符

str.ToUpper()/ToLower()

变大变小

str.Trim()

清除前后空格

str[n]

可直接通过索引访问相应字符

str.IndexOf(string str)

搜索字符串

4、命名空间的使用

namespace 命名空间名{
命名空间体;
}

命名空间隐式使用public修饰符,系统通过命名空间来组织程序。所有的C#程序都具有一个全局命名空间,程序中的所有成员都属于该命名空间。使用visual.studio.NET模板创建C#应用程序时会自动为用户声明一个与项目名称相同的命名空间。

using [别名=]命名空间名[.类型名]

五、继承与接口🎍

1、继承

(1)继承的实现

我们通过下列形式实现继承。

类修饰符 class 类名:基类{
类体;
}

(2)base关键字

base有两种基本用法:指定创建派生类实例时应调用的基类构造函数、用于调用基类构造函数完成对基类成员的初始化工作。通过base可以直接访问直接基类和间接基类中定义的方法和域,base指的是对象不能在static环境下使用。类似于Java中的super。

public class Child:Parent{
public Child():base(参数){
构造体;
}//指定调用的基类构造函数
public void Method(){
base.Method();//调用父类方法
}
}

(3)override与virtual

​ 当一个实例方法声明包含一个override限定符时,这个方法就用相同是属性覆盖一个被继承的​​虚拟方法​​。被覆盖的方法是虚拟的、抽象的方法,覆盖声明及被覆盖的基本方法具有相同的声明访问性。

当方法声明中包含virtual修饰符时该方法为虚方法,当没有virtual修饰符时,方法被称为非虚方法,虚方法中不能包含static、abstract或override修饰符。非虚方法的执行是不变的,而虚方法的执行可以被派生类改变,派生类在重新定义虚方法时需要加上override关键字,要求方法名称、返回值类型、参数表的参数个数、类型顺序都要与被实现的虚方法相同。

2、接口

(1)接口定义

[接口修饰符] interface 接口名 [:基类接口名]{
接口成员;
}

接口的修饰符可以为new、public、protected、internal和private。接口的继承同类的继承相似,private和internal类型不能被继承。接口的成员只能是方法、属性、索引指示器和事件。没有常量、域、操作符、构造函数和析构函数,不能包含任何静态成员。

接口成员默认都是公有访问,接口成员不能被人后修饰符修饰。

(2)接口属性

[接口属性修饰符] 接口属性类型 接口属性标识{[get;][set;]}

(3)接口事件

[接口事件修饰符] [new] event 类型名 接口事件标识符;

(4)接口索引

[接口索引修饰符] [new] 类型名 this [参数列表]{[get;][set;]}

(5)接口成员的访问

在多继承情况下,如果两个父接口含有同名的成员则会产生二义性,需要进行显式声明。

interface A{
int Count{get;set;}
}
interface B{
void Count(int i)
}
interface C:A,B{}

class TestMain{
void Test(C c){
((A)c).count = 1;
((B)c).Count(1);
}
}

3、接口的实现

当使用类来实现接口时,接口的名称必须包含在类声明的基类列表中。如果类实现了某个接口,它也必然隐式地继承了该接口的所有父接口。

(1)显式继承

显式接口成员执行体和其他成员有着不同的访问方式,不能再方法调用、属性访问以及索引指示器访问中通过完全有效名称访问。显式接口成员执行体的使用通常是为了避免接口成员直接因为同名而发生混淆。如果一个类希望对名称和返回类型相同的接口成员采用不同的实现方式,就必须使用显式接口成员执行体。

只有类在声明时把接口名写在了基类列表中,显式接口成员执行体才是有效的。

(2)接口映射

类必须为在基类列表中列出的所有接口的成员提供具体是实现,在类中定位接口成员的实现,称为接口映射。类的成员A是接口成员B的实现的话,则A和B的名称、类型、[参数类型及参数个数]应当一致。

在抽象类中可将接口方法映射成自己的抽象方法(abstract),将责任推给具体子类。也可以拿掉abstract并添加方法主体。

(3)抽象类

和抽象类一样,抽象类也必须提供在基类列表中出现的所有接口成员的实现,不同的是抽象类允许将接口的方法映射到抽象的成员方法中。

interface A{
void Method();
}
abstract class B:A{
public abstract void Method();
}

六、泛型与Lambda🎐

1、泛型

(1)泛型定义

C#泛型类和结构要求用尖括号声明泛型类型参数以及提供泛型类型实参。

泛型类的定义如下。

public class ClassName<T>{
private T item{get;set;}
private T[] items{get;}
public void Method(T data){}
}

泛型促进了类型安全,其确保在参数化的类中,只有成员明确希望的数据类型才可使用。编译时类型检查减小了在运行时发生IncalidCastException异常的概率。为泛型类成员使用值类型,不再造成到object的装箱转换。

泛型接口定义如下。

interface IName<T>{
T field{get;set;}
}

泛型结构定义如下。

public struct SName<T>{
T field{get;set;}
}

(2)泛型约束where

某些特定情况下我们需要限制泛型的类型,此时需要用到where关键字来声明约束。

public class ClassName<T>{
where T:xxxClass
private T item{get;set;}
private T[] items{get;}
public void Method(T data){}
}

(3)泛型方法

public T Method<T>(T item1,T item2)
where T:xxxClass{
T result;
return result;
}

//调用
Method<Class>(item1,item2);

2、Lambda

(1)语句Lambda

(形参列表)=>{语句块;}

语句Lambda的语法比完整的方法声明简单得多,可以不指定方法名、可访问性和返回类型,特定情况下甚至可以不指定参数类型。

bool flag = (a,b)=>{
a = 2;
b = 1;
return a>b;}

(2)表达式Lambda

(形参列表)=>表达式

表达式Lambda是语句Lambda的进一步简化,它只包含要返回的表达式,没有语句块。

bool flag = (a,b) => a>b ;

————
参考资料:

【1】《C#7.0本质论》.机械工业出版社.马克·米凯利斯

【2】《C#程序设计教程》.电子工业出版社

欢迎关注,这里依旧是未来村村长👨‍🌾~
————