类(Class)#
类中的数据和函数称为类的成员,除了这些成员外,类还可以包含嵌套的类型(如其它类),成员的可访问性有:
publicprotectedinternalprotectedprivateinternal
数据成员#
数据成员指包含类的数据:字段、常量和事件的成员,数据成员可以是静态成员,类成员总是实例成员,除非用 static 进行显式声明。
函数成员#
函数成员提供了操作类中数据的某些功能,包括方法、属性、构造函数和终结器、运算符以及索引器
- 方法是与某个类相关的函数,与数据成员一样,函数成员默认为实例成员,使用
static修饰符可以把方法定义为静态方法 - 属性是可以从客户端访问的函数组,访问方式与访问类的公共字段类似,
C#为读写类中的属性提供了专用语法,所以不必使用名称中嵌有Get或set的方法 - 构造函数是在实例化对象时自动调用的特殊函数,必须与所属的类同名,且不能有返回类型,构造函数用于初始化字段的值
- 终结器类似于构造函数,在
CLR检测到不再需要某个对象时调用它,它们名称与类相同,但前面有一个~ - 运算符执行的最简单的操作就是加法和减法。 两个整数相加时严格地说是对整数使用
+运算符,C#允许指定把已有的运算符应用于自己的类(运算符重载) - 索引器允许对象以数组或集合的方式进行索引
方法(Method)#
方法声明#
C#中方法的定义包括:任意方法修饰符(如方法的可访问性)、返回值的类型,然后依次是方法名和输入参数的列表和方法体。每个参数都包括参数的类型名和在方法体中的引用名称。 如果方法有返回值, return 语句就必须与返回值一起使用。
[modifiers] return_type MethodName([parameters])
{
// Method body
}class Test
{
/// <summary>
/// 实例方法
/// </summary>
public void Show()
{
Console.WriteLine("Test下实例方法");
}
/// <summary>
/// 静态方法属于类本身
/// </summary>
public static void Show2()
{
Console.WriteLine("Test下静态方法");
}
}方法调用#
在实例化类得到类的对象后,即可通过 对象.方法名称 进行方法调用。
// 实例方法调用
Test test = new Test();
test.Show();// 静态方法调用
Test.Show2();注意:使用
static修饰的方法属于类的本身,无法使用类的实例化对象进行调用,使用类名.方法名即可
表达式方法体#
如果方法的实现只有一条语句,C# 为方法定义提供了一个简化的语法:表达式体方法,使用 => 区分操作符左边的声明和操作符右边的实现代码。
public void PrintName() => Console.WriteLine(_name);命名参数#
参数一般需要按定义的顺序传送给方法,命名参数允许按任意顺序传递。
class Program
{
static void Main(string[] args)
{
//下面两种调用方式结果是一样的
Print("hello", "world");
Print(b: "world", a: "hello");
Console.ReadKey();
}
static void Print(string a, string b)
{
Console.WriteLine("{0},{1}", a, b);
}
}可选参数#
参数也可以是可选的,必须为可选参数提供默认值,可选参数还必须是方法定义的最后一个参数。
static void Print(string a, string b = "world")
{
Console.WriteLine("{0},{1}", a, b);
}Print("hello");//输出hello,world
Print("hello","wang");//输出hello,wang个数可变的参数#
使用 params[] 数组的方式可以定义数量可变的参数,如果 params[] 关键字与方法签名定义的多个参数一起使用,则只能使用一次,而且必须是最后一个参数。
方法重载#
方法名相同,但参数的个数或类型不同。
static void Print(string a)
{
Console.WriteLine("{0}", a);
}
static void Print(string a, string b = "world")
{
Console.WriteLine("{0},{1}", a, b);
}注意:
两个方法不能仅在返回类型上有区别
两个方法不能仅根据参数是声明为 ref 还是 out 来区分
局部函数#
局部函数只能在声明该局部函数的方法内部调用
public void LocalMethod()
{
static int Add(int x, int y) => x + y;
int result = Add(1, 2);
Console.WriteLine(result);
}扩展方法#
扩展方法允许创建扩展其他类型的方法。扩展方法需要是静态方法,并且在静态类中声明。
枚举#
枚举是值类型,包含一组命名的常量
public enum Colors
{
Red = 1,
Green = 2,
Blue = 3
}属性(Property)#
它是一个方法或一对方法,在客户端代码看来,它(们)是一个字段。
属性定义#
public string SomeProperty
{
get
{
return "This is the property value";
}
set
{
//type string
}
}只读和只写属性#
在属性定义中省略 set 访问器,就可以创建只读属性;同样在属性定义中省略 get 访问器,就可以创建只写属性。
class Program
{
private string age;
private string name;
//只读属性
public string Age
{
get
{
return age;
}
}
//只写属性
public string Name
{
set
{
value= name;
}
}
}属性的访问修饰符#
C# 允许给属性的 get 和 set 访问器设置不同的访问修饰符,所以属性可以有公有的 get 访问器和私有或受保护的 set 访问器。有助于控制属性的设置方式。
自动实现的属性#
如果属性的 get 和 set 访问器中没有任何逻辑,就可以使用自动实现的属性,使用自动实现的属性,就不能在属性设置中验证属性的有效性。
class Program
{
public string Age { get; set; }
public string Name { get; set; }
static void Main(string[] args)
{
Console.ReadKey();
}
}构造函数#
声明基本构造函数的语法就是声明一个与包含的类同名的方法,但该方法没有返回类型。
构造函数声明#
class Person
{
Person(){}
}注意:没有必要给类显式提供构造函数,原因在于:如果没有在类中没有提供任何构造函数,编译器会在后台创建一个默认的无参构造函数用来把所有的成员字段初始化为标准的默认值。
构造函数重载#
构造函数重载遵循与其他方法相同的规则,就是说允许为构造函数提供任意多的重载。
class Program
{
static void Main(string[] args)
{
//调用无参构造函数实例化对象
Person person1 = new Person();
//调用带参数的构造函数实例化对象
Person person2 = new Person(3);
//因为带name参数的构造函数是private的,所以这里无法实例化
//Person person3 = new Person(20, "wang");
Console.ReadKey();
}
}
class Person
{
private int age;
private string name;
public Person()
{
}
public Person(int age)
{
//使用this关键字区分成员字段和同名参数
this.age = age;
}
private Person(int age, string name)
{
this.age = age;
this.name = name;
}
} class Person
{
private int age;
private string name;
private Person(int age, string name)
{
this.age = age;
this.name = name;
}
}这个例子没有为 Person 类定义任何公有的或受保护的构造函数。这就使 Person 不能使用 new 运算符在外部代码中实例化(但可以在 Person 中编写一个公有静态属性或方法,以实例化该类)。 这在下面两种情况下是有用的:
- 类仅用作某些静态成员或属性的容器,因此永远不会实例化它
- 希望类仅通过调用某个静态成员函数来实例化(单例模式)
注意:如果提供了带参数的构造函数,编译器就不会隐式的自动创建默认的构造函数。
构造函数初始化器#
有时,在一个类中有几个构造函数,以容纳某些可选参数,这些构造函数包含一些共同的代码。需要做到从构造函数中调用其他构造函数时可以使用构造函数初始化器。
class Program
{
static void Main(string[] args)
{
Person person1 = new Person(20);
Person person2 = new Person(20, "li");
Console.WriteLine("person1:age={0},name={1}", person1.age, person1.name);
Console.WriteLine("person2:age={0},name={1}", person2.age, person2.name);
Console.ReadKey();
}
}
class Person
{
public int age;
public string name;
public Person(int age)
{
this.age = age;
this.name = "wang";
}
public Person(int age, string name)
{
this.age = age;
this.name = name;
}
}上面的例子是一个简单的构造函数重载,然后通过调用不同的构造函数实例化对象,Person 类的两个构造函数初始化了相同的字段 age ,显然最好把所有的代码放在一个地方,C#中使用构造函数初始化器,可以实现此目的:
class Program
{
static void Main(string[] args)
{
Person person1 = new Person(20);
Person person2 = new Person(20, "li");
Console.WriteLine("person1:age={0},name={1}", person1.age, person1.name);
Console.WriteLine("person2:age={0},name={1}", person2.age, person2.name);
Console.ReadKey();
}
}
class Person
{
public int age;
public string name;
public Person(int age)
{
this.age = age;
this.name = "wang";
}
public Person(int age, string name) : this(age)
{
this.age = age;
this.name = name;
}
}这里 this 关键字仅调用参数最匹配的那个构造函数。
注意:构造函数初始化器在构造函数的函数体之前执行。C#构造函数初始化器可以包含对同一个类的另一个构造函数的调用(使用前面介绍的语法),也可以包含对直接基类的构造函数的调用(使用相同的语法,但应使用 base 关键字代替 this。初始化器中不能有多个调用。
静态类#
如果类只包含静态的方法和属性,该类就是静态的。静态类在功能上与使用私有静态函数创建的类相同,不能创建静态类的实例。
静态构造函数#
静态构造函数只执行一次,而前面的构造函数是实例构造函数,只要创建类的对象,就会被执行。编写静态构造函数的一个原因是,类有一些静态字段或属性,需要在第一次使用类之前,从外部源中初始化这些静态字段和属性。
注意:.Net 运行库没有确保什么时候执行静态构造函数,所以不应把要求在某个特定时刻(例如,加载程序集时)执行的代码放在静态构造函数中。也不能预计不同类的静态构造函数按照什么顺序执行。但是,可以确保静态构造函数最多运行一次,即在代码引用类之前调用它
在C#中,通常在第一次调用类的任何成员之前执行静态构造函数。静态构造函数没有访问修饰符,其他C#代码从来不调用它,但在加载类时,总是由.NET运行库调用它,所以像 public 或 private 这样的访问修饰符就没有任何意义。出于同样原因,静态构造函数不能带任何参数,一个类也只能有一个静态构造函数。很显然,静态构造函数只能访问类的静态成员,不能访问类的实例成员。
无参数的实例构造函数与静态构造函数可以在同一个类中同时定义。尽管参数列表相同,但这并不矛盾,因为在加载类时执行静态构造函数,而在创建实例时执行实例构造函数,所以何时执行哪个构造函数不会有冲突。如果多个类都有静态构造函数,先执行哪个静态构造函数就不确定。此时静态构造函数中的代码不应依赖于其他静态构造函数的执行情况。 另一方面,如果任何静态字段有默认值,就在调用静态构造函数之前指定它们。
参数传递#
C#中,方法、构造函数可以拥有参数,当调用方法或者构造函数时,需要提供参数,而参数的传递方式有两种(以方法为例):
按值传递#
值类型对象传递给方法时,传递的是值类型对象的副本而不是值类型对象本身
public void ParamByVal()
{
static void SetIntValue(int i)
{
i += 100;
}
int i = 1;
SetIntValue(i);
// i=1,i是值类型按值传递,此处传递给方法的其实是变量i的副本而不是对象本身
Assert.AreEqual(1, i);
}按引用传递#
对于引用类型对象,其实也是按值传递的,但是不像值类型传递的是一个副本,引用类型传递的是引用地址。在方法中使用这个地址去修改对象的成员,自然就会影响到原来的对象
注意:如果值类型对象中含有引用类型的成员,那么当值类型对象在传递给方法时,副本中克隆的是引用类型成员的地址,而不是引用类型对象的副本,所以在方法中修改此引用类型对象成员中的成员等也会影响到原来的引用类型对象。
public void ParamByReference()
{
static void SetIntArrValue(List<int> arr)
{
arr.Add(100);
}
var arr = new List<int>() { 1, 2, 3, 4, 5 };
Console.WriteLine(JsonConvert.SerializeObject(arr));
SetIntArrValue(arr);
// [1,2,3,4,5,100]
Console.WriteLine(JsonConvert.SerializeObject(arr));
}特殊情况string#
尽管 string 属于引用类型,但它在参数传递时表现出了按值传递的特色
public void ParamByString()
{
static void SetStringValue(string oldStr)
{
oldStr = "world";
}
string str = "hello";
SetStringValue(str);
// 此处正常理解应该返回world,因为str是引用类型。
// 但其实因为string的“不变性”,所以在被调用方法中执行 oldStr="world"时,此时并不会直接修改oldStr中的"hello"值为"world",因为string类型是不变的,不可修改的
// 此时内存会重新分配一块内存,然后把这块内存中的值修改为 “world”,然后把内存中地址赋值给oldStr变量,但此时str仍然指向 "hello"字符,而oldStr却改变了指向,它指向了"world"字符串
Console.WriteLine(str);
}实际引用传递#
引用传递可以理解为就是对象本身传递,而非一个副本或者地址,一般使用 in、out、ref 关键字声明参数是引用传递。
in#
in 修饰的参数表示参数通过引用传递,但是参数是只读的,所以在调用方法时必须先初始化
public void ParamByIn()
{
static void SetIntValue(in int i)
{
// i += 100; // 因为in修饰的参数为只读所以不能直接赋值
Console.WriteLine($"{i}");
}
int i = 1;
SetIntValue(i);
// i=1,虽然是按引用传递但是in修饰的参数是只读所以无法在方法内部为i赋值
Assert.AreEqual(1, i);
}ref#
ref 关键字使参数按引用传递。其效果是,当控制权传递回调用方法时,在方法中对参数的任何更改都将反映在该变量中。
若要使用 ref 参数,则方法定义和调用方法都必须显式使用 ref 关键字
public void ParamByRef()
{
static void SetIntValue(ref int i)
{
i += 100; // ref修饰的参数会按引用传递
}
int i = 1;
SetIntValue(ref i); // ref修饰的参数方法定义和调用方法都必须显式使用ref关键字
Assert.AreEqual(101, i);
}out#
out 关键字使参数按引用传递。与ref关键字类似,不同之处在于ref要求变量必须在传递之前进行初始化而out不需要但是在方法返回前必须为参数赋值。
若要使用 out 参数,方法定义和调用方法都必须显式使用out关键字
public void ParamByOut()
{
static void SetIntValue(out int i)
{
i = 100; // out修饰的参数会按引用传递,在方法返回前必须给i赋值
}
SetIntValue(out int i); // out参数,方法定义和调用方法都必须显式使用out关键字
Assert.AreEqual(100, i);
}- ref 和 in都是引用传递,而且要求调用方法前需要提前初始化,但是与in不同的是,调用时ref关键字不能省略,且参数必须是变量,不能是常量
- ref 和 out都是引用传递,且在调用时候,ref 和 out 关键字不能省略,且参数必须是变量,不能是常量,但是ref要求调用方法前需要提前初始化,且无需在调用方法结束前赋值
- 与 in 和 out 不同的是,在调用方法中时,可以读写整个 ref 参数对象及它的成员
结构#
结构是值类型#
结构是会影响性能的值类型,但根据使用结构的方式,这种影响可能是正面的,也可能是负面的。
- 正面影响是:为结构分配内存时,速度非常快,因为它们将内联或者保存在栈中。 在结构超出了作用域被删除时,速度也很快
- 负面影响是:只要把结构作为参数来传递或者把一个结构赋予另一个结构(如A-B,其 中A和 B是结构),结构的所有内容就被复制,而对于类,则只复制引用。 这样就会有性能损失,根据结构的大小,性能损失也不同
注意:结构主要用于小的数据结构。但当把结构作为参数传递给方法时,应把它作为
ref参数传递,以避免性能损失,此时只传递了结构在内存中的地址,这样传递速度就与在类中的传递速度一样快了。但如果这样做,就必须注意被调用的方法可以改变结构的值
结构不支持继承#
结构(和C#中的其他类型一样)最终派生于类 System.Object。因此结构也可以访问 System.Object 的方法。在结构中,甚至可以重写 System.Object 中的方法(如重写Tostring)方法。 结构的继承链是:每个结构派生自 System.ValueType 类 ,System.ValueType 类又派生自 System.Object 。 ValueType 并没有给 Object 添加任何新成员,但提供了一些更适合结构的实现方式。
注意:不能为结构提供其他基类,每个结构都派生自
ValueType
结构的构造函数#
为结构定义构造函数的方式与为类定义构造函数的方式相同,但不允许定义无参数的构造函数。
注意:.Net运行库禁止在C#结构内定义无参构造函数
类和结构的区别#
结构与类的区别在于它们在内存中的存储方式、访问方式(类是存储在堆上的引用类型,而结构是存储在栈上的值类型)和它们的一些特征(如结构不支持继承。较小的数据类型使用结构可提高性能。但在语法上,结构与类非常相似,主要的区别是使用关键字 struct 代替 class 来声明结构。对于类和结构,都使用关键字 new 来声明实例创建对象并对其进行初始化。
匿名类型#
匿名类型只是一个继承自 Object 且没有名称的类。该类的定义从初始化器中推断,类似于隐式类型化的变量。
var person = new { Name = "wang", Age = 22 };继承#
在面向对象编程中,有两种截然不同的继承类型,实现继承和接口继承。C# 不支持多重继承但可以派生自另一个类和任意多的接口。
- 实现继承:表示一个类型派生自一个基类型,它拥有该基类型的所有成员字段和函数,在需要给现有类型添加功能或者许多相关类型共享一组重要的公共功能时这种类型继承非常有用
- 接口继承:表示一个类型只继承了函数的签名,没有继承任何的实现代码
实现继承#
/// <summary>
/// 基类
/// </summary>
class Person
{
/// <summary>
/// 使用virtual关键字定义的方法允许在派生类中使用override重写
/// </summary>
public virtual void SayHello()
{
Console.WriteLine("基类的SayHello");
}
}
/// <summary>
/// 派生自Person
/// </summary>
class ChinaPerson : Person
{
/// <summary>
/// 使用override关键字重写基类的SayHello方法
/// </summary>
public override void SayHello()
{
Console.WriteLine("你好");
}
}
/// <summary>
/// 派生自Person
/// </summary>
class ThailandPerson : Person
{
public override void SayHello()
{
Console.WriteLine("萨瓦迪卡");
}
}把一个基类函数声明为 virtual,就可以在任何派生类中重写该函数,virtual 也适用于属性。
注意:成员字段和静态函数都不能声明为
virtual,因为这个概念只对类中的实例成员有意义
接口继承#
表示一个类型只继承了函数的签名,没有继承任何实现代码。在需要指定该类型具有某些可用的特性时,最好使用这种类型的继承。接口名称通常以字母 I 开头,以便知道这是一个接口。C#支持多接口继承和单一实现继承,接口继承中又分为隐式实现和显式实现。
interface IPerson
{
void SayHello();
}
/// <summary>
/// 隐式实现接口
/// </summary>
class ChinaPerson : IPerson
{
public void SayHello()
{
Console.WriteLine("你好");
}
}
/// <summary>
/// 显式实现接口
/// </summary>
class ThailandPerson : IPerson
{
void IPerson.SayHello()
{
Console.WriteLine("莎娃迪卡");
}
}对于隐式实现的接口调用这两种方式都可以:
ChinaPerson chinaPerson = new ChinaPerson();
IPerson person = new ChinaPerson();
person.SayHello();
chinaPerson.SayHello();对于显式实现的接口调用只能使用接口调用:
IPerson thailandPerson = new ThailandPerson();
thailandPerson.SayHello();隐藏方法#
如果签名相同的方法在基类和派生类中都进行了声明,但该方法没有声明为 virtual 和 override,派生类会隐藏基类方法。
class Person
{
public void SayHello()
{
Console.WriteLine("基类的SayHello");
}
}
class ChinaPerson : Person
{
//提示:隐藏继承的成员Person.SayHello,如果有意的,请使用关键字new
public void SayHello()
{
Console.WriteLine("你好");
}
}调用基类方法#
C#中可以使用 base. 这种语法来调用方法的基类版本。
class Person
{
public virtual void SayHello()
{
Console.WriteLine("基类的SayHello");
}
}
class ChinaPerson : Person
{
public override void SayHello()
{
base.SayHello();
Console.WriteLine("你好");
}
}派生类构造函数执行#
首先定义基类 A,为了方便查看,显式指明基类的无参构造函数
class A
{
public int Age { get; set; }
public string Name { get; set; }
public A()
{
Console.WriteLine("A类无参构造函数");
}
}然后定义B类继承自A类
class B : A
{
public B()
{
Console.WriteLine("B类无参构造函数");
}
}实例化B类
static void Main(string[] args)
{
B b = new B();
Console.ReadKey();
}输出: A类无参构造函数 B类无参构造函数
实例化子类时,只可以
new子类,执行顺序为:先执行父类构造函数=>再执行子类构造函数
如果父类存在多个构造函数会怎么样?
class A
{
public int Age { get; set; }
public string Name { get; set; }
public A()
{
Console.WriteLine("A类无参构造函数");
}
public A(int age, string name)
{
Console.WriteLine("A类带参构造函数");
}
}
class B : A
{
public B()
{
Console.WriteLine("B类无参构造函数");
}
}再次实例化B类
static void Main(string[] args)
{
B b = new B();
Console.ReadKey();
}输出: A类无参构造函数 B类无参构造函数
实例化子类时,会先执行父类的构造函数(默认为父类的无参构造函数),也可以在子类中使用
base关键字指定调用父类的哪个构造函数
class Program
{
static void Main(string[] args)
{
B b = new B(3);
Console.ReadKey();
}
}
class A
{
public int Age { get; set; }
public A()
{
Console.WriteLine("A类无参构造函数");
}
public A(int age)
{
Console.WriteLine("A类带参构造函数");
}
}
class B : A
{
public B() : base(3)
{
Console.WriteLine("B类无参构造函数调用父类带参构造函数");
}
}输出: A类带参构造函数 B类无参构造函数调用父类带参构造函数
总结:
- 实例化父类时,可以使用
new子类,执行构造函数顺序为:执行父类构造函数=>执行子类构造函数 - 实例化子类时,只可以
new子类,执行顺序同上 - 父类实例化后,只能执行父类的方法,获得父类的属性等
- 实例化子类后,可同时执行子类和父类的方法和属性,如同名方法,则执行子类的方法
- 子类构造函数可以使用
base关键字指定调用的父类构造函数
类和结构都是创建对象的模板,每个对象都包含数据,并提供了处理和访问数据的方法。类定义了类的每个对象(称为实例)可以包含什么数据和功能。还可以定义处理在这些字段中存储的数据的功能。
抽象类和抽象函数#
- C#中允许把类或函数声明为
abstract,抽象类不能被实例化。抽象函数也不能直接实现,必须在非抽象的派生类中重写 - 如果类包含抽象函数,则该类也必须被声明为抽象的
- 抽象方法只在派生类中真正实现,这表明抽象方法只存放函数原型不涉及主体代码
- 派生自抽象类的类需要实现其基类的抽象方法,才能实例化对象
- 使用
override关键字可在派生类中实现抽象方法,经override声明重写的方法称为重写基类方法,其签名必须与override方法的签名相同
密封类和密封方法#
C#允许把类和方法声明为 sealed ,对于类这表示不能继承。对于方法这表示不能重写该方法。
抽象类和接口的区别#
相同点
- 都可以被继承
- 都不能被实例化
- 都包含方法声明
- 派生类必须实现未实现的方法
区别
- 抽象基类可以定义字段/属性/方法实现.接口只能定义属性/索引器/事件/方法声明
- 抽象类是一个不完整的类,需要通过集成进一步细化。而接口更像是一个行为规范表明能做什么
- 接口是可以被多重实现的,可以有多个类实现接口,因为类的单一继承性,抽象类只能被单一继承
- 抽象类实现继承需要使用
override关键字,接口则不用 - 如果抽象类实现接口,可以把接口方法映射到抽象类中作为抽象方法不必实现,而在抽象类的子类中实现接口方法
- 抽象类表示的是这个对象是什么;接口表示的是这个对象能做什么;使用抽象类是为了代码的复用,使用接口是为了实现多态性
普通类和抽象类的区别#
- 都可以被继承
- 抽象类不能实例化,普通类允许实例化
- 抽象方法只包含方法声明而且必须包含在抽象类中
- 子类继承抽象类必须实现抽象类中的抽象方法除非子类也是抽象类
- 抽象类中可以包含抽象方法和实例方法