C#基础语法
编译器功能介绍
使用VS创建一个控制台程序,其界面如下:

左侧是解决方案,右侧是代码输入框。下侧是控制台,可以输入命令来控制VS,例如可以输入.NET run启动Vs的编译功能。
什么是解决方案
解决方案是项目文件的一个集合,其内部可以创建多个项目。每一个项目只能有一个入口,对于控制台项目其主要表现形式是在控制台中可以输出。在当前页面中是一个顶级语句的Hello World,当我们执行时可以在控制台中输入dotnet run或者在图形化界面点击启动,其就会在控制台中输出:Hello World。其中,使用命令行启动程序需要找到目标路径的.csproj文件。

什么是
.csproj文件
.csproj文件,是XML文件。csproj文件包括与目标.NET Framework,项目文件夹,NuGet程序包引用等相关的设置。接下来参考这篇文章来进行学习

新版本的.csproj文件是旧版本文件的升级,旧版本的如下:

编译内容常规结构
C# 程序由一个或多个文件组成。 每个文件都包含零个或多个命名空间。 命名空间包含类、结构、接口、枚举和委托或其他命名空间等类型。 下面的示例是包含所有这些元素的 C# 程序的框架。其中命名空间是为了让标识符可重用,一个标识符可在多个命名空间中定义,它在不同命名空间中的含义是互不相干的。这样,在一个新的命名空间中可定义任何标识符,它们不会与任何已有的标识符发生冲突,因为已有的定义都处于其他命名空间中。常见的一个结构如下:
using System;
// 顶级语句Console.WriteLine("Hello world!");
namespace YourNamespace{ class YourClass { }
struct YourStruct { }
interface IYourInterface { }
delegate int YourDelegate();
enum YourEnum { }
namespace YourNestedNamespace { struct YourStruct { } }}其中需要注意的是,顶级语句的优先级高于定义的Main程序,当我们在一个项目中创建一个新的程序,在这个程序中写入一个Main入口,结果编译的结果还是顶级语句输出的结果。此外C# 9 引入顶级语句是为了简化控制台程序,不需要再写模板代码。所以使用顶级语句是为了测试

类型
值类型和引用类型是 C# 类型的两个主要类别。 值类型的变量包含类型的实例。 它不同于引用类型的变量,后者包含对类型实例的引用。 默认情况下,在分配中,通过将实参传递给方法并返回方法结果来复制变量值。 对于值类型变量,会复制相应的类型实例。
参考这篇博客的说法:所有的值类型都继承自System.ValueType(System.ValueType继承自System.Object),也就是说,所有继承自System.ValueType的类型都是值类型,而其他类型都是引用类型。
引用和值类型
引用类型和值类型,他们的区别就在于存储在的抽象数据结构的不同。值类型存储在栈中,引用类型存储在堆中。栈是一种先进后出的数据结构,堆是一种完全二叉树。讨论这二者类型,需要从内存入手:
假设有一个内存区域,我们在声明值类型的时候会取得一块连续的区域作为栈空间,栈空间指挥着这些数据完成程序操作,我们执行一个简单程序的时候,会将整个程序按照先进后出的效果压入栈中,例如逆波兰计算机表达式(事实上,计算值类型数据压入栈的计算方式确实很像)。堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间(可以不连续)。即动态分配内存,对其访问和对一般内存的访问没有区别。
而在运行整体程序的时候,常提到堆栈,实际上堆栈就是栈。在这篇博客中,堆栈的特性是最后一个放入堆栈中的物体总是被最先拿出来, 这个特性通常称为后进先出(LIFO)队列。 堆栈中定义了一些操作。 两个最重要的是PUSH和POP。 PUSH操作在堆栈的顶部加入一 个元素。POP操作相反, 在堆栈顶部移去一个元素, 并将堆栈的大小减一。

而至于在C#中,堆栈对操作系统和内存的调度,由于本人水平有限,可以看这篇文章.
结构体
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;
namespace C__basic_syntax{ internal class ToLearnStructure {
public struct Coords { public Coords(double x, double y) { X = x; Y = y; }
public double X { get; } public double Y { get; }
// 重写 ToString 方法以便更好地显示坐标 public override string ToString() => $"({X}, {Y})"; }
// 在 readonly 结构中,可变引用类型的数据成员仍可改变其自身的状态。 // 这个字段在 声明时 或 构造函数里 可以赋值。之后就不能再修改。 public readonly struct Coord { public Coord(double x, double y) { X = x; Y = y; }
public double X { get; init; } public double Y { get; init; }
public override string ToString() => $"({X}, {Y})"; }
public static void Main() { // 1. 测试原结构体 Coords point = new Coords(3.5, 7.2); Console.WriteLine(point); // 输出: (3.5, 7.2)
// 2. 测试只读结构体 Coord coord = new Coord(4.5, 9.3); // coord.X = 5.0; // 这行会报错,因为 X 是只读的 Console.WriteLine(coord); // 输出: (4.5, 9.3) }
}}枚举
枚举类型是自命名的值类型,在其中我们可以插入想要的数据,并通过类似声明对象的方式引用其中保存的数据项。
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;
namespace C__basic_syntax{ enum Season { Spring, Summer, Autumn, Winter } // 指定这个枚举底层使用 16 位无符号整数 (ushort) 存储 enum ErrorCode : ushort { None = 0, Unknown = 1, ConnectionLost = 100, OutlierReading = 200 }
internal class ToLearnEnum {
class Project { static void Main(string[] args) { // 使用枚举中的值 Season season = Season.Autumn; Console.WriteLine(season); // 输出: Autumn } }
}}类
类是一种可以声明对象的实体,我们可以在类中声明各种结构,如变量,结构体等等。类的作用是增加封装性,实现面向对象的方法。类是一种引用类型,在没有使用new标识符时,访问类只会返回null。一个类有许多封装类型修饰符,如下:
using System;
namespace DemoNamespace{ // 只能是 public 或 internal public class PublicClass { } // 当前程序集(assembly) internal class InternalClass { }
// 功能性修饰符的组合
// 抽象类需要重写其功能 public abstract class AbstractClass { }
// 密封类(sealed class)无法被继承 public sealed class SealedClass { }
// 静态类只能包含静态成员,且无法被实例化或继承 public static class StaticClass { }
// 可以在另一个文件里继续定义 internal partial class PartialClass { }
// 允许不安全代码 // internal unsafe class UnsafeClass { }}下面是一个猫咪类,包含了类的基本使用方法:
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;
namespace C__basic_syntax{ class Cat { // 变量最好用下底开头_ int _Years; string _Feature;
// 特征字符串数组 string[] features = { "白色", "灰色", "橘黄" };
public int Years => _Years; // 新增公开属性用于访问年龄, 所有类中的变量最好都需要一个属性来供外界访问 public string Feature => _Feature;
public Cat() { _Years = Random.Shared.Next(5, 8); // 生成一个随机整数 _Feature = features[Random.Shared.Next(features.Length)]; // 从特征数组中随机选择一个 }
public void Call() { Console.WriteLine("喵喵"); } }
// 所有代码都必须属于某个类型,也就是说Main需要在一个类方法中才可以被调用 class Project { static void Main(string[] args) { Cat cat = new Cat(); // 在Main方法中创建Cat对象 Console.WriteLine("猫的年龄是{0}", cat.Years); // 通过公开属性访问年龄 Console.WriteLine("猫的特征是{0}", cat.Feature); cat.Call(); // 调用Cat对象的方法 } }}命名空间
namespace 关键字用于声明包含一组相关对象的作用域。 可以使用命名空间来组织代码元素并创建全局唯一类型。
namespace SampleNamespace{ class SampleClass { }
interface ISampleInterface { }
struct SampleStruct { }
enum SampleEnum { a, b }
delegate void SampleDelegate(int i);
namespace Nested { class SampleClass2 { } }}在不同作用域中,可以使用.操作符来调用对应命名空间的方法。
namespace ToLearnNamespace{ public class ToLearnClass { public void ToLearnMethod() { System.Console.WriteLine("你好我是学习类"); } }}
namespace chara{ class Program { static void Main(string[] args) { // 使用.操作符来访问命名空间中的类和方法 ToLearnNamespace.ToLearnClass obj = new ToLearnNamespace.ToLearnClass(); obj.ToLearnMethod(); } }
}对于指定方法的命名空间,可以使用Using标识符来引用对应的命名空间。
接口
接口是一种构造方案,对于继承这个接口的方法或者类,需要符合这个接口的一些特性,才可以对其继承。可以使用interface 来命名一个接口,按照约定,接口名称以大写字母 I 开头。
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;
namespace C__basic_syntax2{ internal class ToLearnInterface { interface IEquatable<T> { // 定义了一个泛型方法 Equals,用于比较当前对象与另一个同类型对象是否相等 bool Equals(T obj); }
public class Car : IEquatable<Car> { public string? Make { get; set; } public string? Model { get; set; } public string? Year { get; set; }
// 实现 IEquatable<Car> 接口的 Equals 方法,如果不实现这个方法,编译器会报错 public bool Equals(Car? car) { return (this.Make, this.Model, this.Year) == (car?.Make, car?.Model, car?.Year); } } }}接口可以包含实例方法、属性、事件、索引器或这四种成员类型的任意组合。 接口可以包含静态构造函数、字段、常量或运算符。 从 C# 11 开始,非字段接口成员可以是 static abstract。 接口不能包含实例字段、实例构造函数或终结器。 接口成员默认是公共的,可以显式指定可访问性修饰符(如 public、protected、internal、private、protected internal 或 private protected)。 private 成员必须有默认实现。
接口可从一个或多个接口继承。 派生接口从其基接口继承成员。 实现派生接口的类必须实现派生接口中的所有成员,包括派生接口的基接口的所有成员。 该类可能会隐式转换为派生接口或任何其基接口。 类可能通过它继承的基类或通过其他接口继承的接口来多次包含某个接口。 但是,类只能提供接口的实现一次,并且仅当类将接口作为类定义的一部分 (class ClassName : InterfaceName) 进行声明时才能提供。 如果由于继承实现接口的基类而继承了接口,则基类会提供接口的成员的实现。
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;
namespace C__basic_syntax2{ internal class ToLearnInterface { interface IEquatable<T> { // 定义了一个泛型方法 Equals,用于比较当前对象与另一个同类型对象是否相等 bool Equals(T obj); }
// 接口中的属性也可以是泛型的 interface Iappearance { string? Color { get; set; } string? Size { get; set; } }
// 继承多个接口,用逗号分隔 public class Car : IEquatable<Car> , Iappearance { public string? Make { get; set; } public string? Model { get; set; } public string? Year { get; set; } public string? Color { get; set; } public string? Size { get; set; }
// 实现 IEquatable<Car> 接口的 Equals 方法,如果不实现这个方法,编译器会报错 public bool Equals(Car? car) { return (this.Make, this.Model, this.Year) == (car?.Make, car?.Model, car?.Year); } } }}类的属性和索引器可以为接口中定义的属性或索引器定义额外的访问器。 例如,接口可能会声明包含 get 取值函数的属性。 实现此接口的类可以声明包含 get 和 get 取值函数的同一属性。 但是,如果属性或索引器使用显式实现,则访问器必须匹配。
匿名
匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。 类型名由编译器生成,并且不能在源代码级使用。 每个属性的类型由编译器推断。
匿名类型使用var修饰符修饰变量,匿名变量会检测输入的变量的数据,来决定最终参与计算的数据类型。
namespace C__basic_syntax2{ // 匿名类型只能在方法内部使用,不能作为方法的返回类型或参数类型 // 匿名类型通常是作为局部变量使用的 class Program {
void Method() { var v = new { Amount = 108, Message = "Hello" }; // 正确 Console.WriteLine(v.Amount + v.Message); }
static void Main(string[] args) { Program p = new Program(); p.Method(); }
}
}委托
**委托是一种类型。**需要注意,delegate是C#中一个关键字,用于声明委托类型。而System.Delegate是.NET基础类库中的一个类型,它是所有委托类型的基类,但是不允许显式地从这个类派生新类。每次我们在使用delegate关键字时,我们都是在定义一个新的类型,而这个类型是Delegate的子类,我们说委托类型时,实际上是指的所有使用delegate定义和派生自Delegate的类型。
**委托可以引用方法,并且可以通过委托来调用其他方法。**很多书、文档、教程经常会说这样的一句话:“委托和C/C++中的函数指针类似,但是是类型安全的”。函数指针只是函数所在的内存地址,通过函数指针可以引用函数。在C#中使用委托类型的实例引用方法,前面提到过,委托是一个类,那么它也有对应的方法和属性,我们可以使用委托类型的实例更加灵活地调用方法。
using System.Linq.Expressions;
namespace _lanbda{
internal class Program { // 定义一个返回字符串的委托 public delegate string MethodReturnString(); public delegate string FunctionReturnString();
public static string SayHello() { return "Hello, World!"; }
static void Main(string[] args) {
MethodReturnString methodReturnString = SayHello; FunctionReturnString functionReturnString = SayHello;
Console.WriteLine(methodReturnString());
// Console.WriteLine(functionReturnString); 打印的数据是 // _lanbda.Program + FunctionReturnString
// 不是相同的类型,无法赋值 // methodReturnString = functionReturnString;
} }}事件
事件就是一个特殊的委托类型的字段。和委托类似,事件是后期绑定机制。 实际上,事件是建立在对委托的语言支持之上的。事件是一种对象广播(向系统中所有感兴趣的组件)广播所发生事件的方法。 任何其他组件都可以订阅事件,并在事件发生时收到通知。
public event EventHandler<FileFoundArgs>? FileFound;如果要引发事件,请使用委托调用语法调用事件处理程序:
FileFound?.Invoke(this, new FileFoundArgs(file));?. 运算符便于确保不尝试在没有事件的订阅服务器时引发该事件。通过使用 += 运算符订阅事件:
var fileLister = new FileSearcher();int filesFound = 0;
EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>{ Console.WriteLine(eventArgs.FoundFile); filesFound++;};
fileLister.FileFound += onFileFound;处理程序方法通常具有前缀“On”,后跟事件名称,如前面的代码所示。使用 -= 运算符取消订阅:
fileLister.FileFound -= onFileFound;模式匹配
关键字
关键字是预定义的保留标识符,对编译器具有特殊含义。 除非它们作为前缀包含 @ ,否则它们不能用作程序中的标识符。 例如, @if 是有效的标识符,但 if 不是因为 if 关键字。
修饰符
访问修饰符
访问修饰符控制结构的开闭关系,控制程序关键字的访问级别,可使用访问修饰符指定以下 7 个可访问性级别:
public:访问不受限制。protected:访问限于包含类或派生自包含类的类型。internal:访问限于当前程序集。protected internal:访问限于当前程序集或派生自包含类的类型。private:访问限于包含类。private protected:访问限于包含类或当前程序集中派生自包含类的类型。file:已声明的类型仅在当前源文件中可见。 文件范围的类型通常用于源生成器。
abstract
abstract 修饰符指示被修改内容的实现已丢失或不完整。 abstract 修饰符可用于类、方法、属性、索引和事件。 在类声明中使用 abstract 修饰符来指示某个类仅用作其他类的基类,而不用于自行进行实例化。 标记为抽象的成员必须由派生自抽象类的非抽象类来实现。
namespace LanguageKeywords;
public abstract class Vehicle{ protected string _brand;
// 构造函数 - 在抽象类中可以有已实现的方法 public Vehicle(string brand) => _brand = brand;
// 已实现的方法 - 提供所有交通工具共有的功能 public string GetInfo() => $"This is a {_brand} vehicle.";
// 另一个已实现的方法 public virtual void StartEngine() => Console.WriteLine($"{_brand} engine is starting...");
// 抽象方法 - 必须由派生类实现 public abstract void Move();
// 抽象属性 - 必须由派生类实现 public abstract int MaxSpeed { get; }}
public class Car : Vehicle{ public Car(string brand) : base(brand) { }
// 抽象方法的实现 public override void Move() => Console.WriteLine($"{_brand} car is driving on the road.");
// 抽象属性的实现 public override int MaxSpeed => 200;}
public class Boat : Vehicle{ public Boat(string brand) : base(brand) { }
// 抽象方法的实现 public override void Move() => Console.WriteLine($"{_brand} boat is sailing on the water.");
// 抽象属性的实现 public override int MaxSpeed => 50;}
public class AbstractExample{ public static void Examples() { // 无法实例化抽象类: Vehicle v = new Vehicle("Generic"); // 错误!
Car car = new Car("Toyota"); Boat boat = new Boat("Yamaha");
// 使用抽象类中已实现的方法 Console.WriteLine(car.GetInfo()); car.StartEngine();
// 使用派生类中对抽象方法的实现 car.Move(); Console.WriteLine($"Max speed: {car.MaxSpeed} km/h");
Console.WriteLine();
Console.WriteLine(boat.GetInfo()); boat.StartEngine(); boat.Move(); Console.WriteLine($"Max speed: {boat.MaxSpeed} km/h"); }}
class Program{ static void Main() { AbstractExample.Examples(); }}async
使用 async 修饰符可将方法、lambda 表达式或匿名方法指定为异步。 如果对方法或表达式使用此修饰符,则其称为异步方法 。 如下示例定义了一个名为 ExampleMethodAsync 的异步方法:
public async Task<int> ExampleMethodAsync(){ //...}异步方法同步运行,直至到达其第一个 await 表达式,此时会将方法挂起,直到等待的任务完成(把控制权交还给调用者。这个时候不会阻塞线程)。
const
使用 const 关键字声明常量字段或本地常量。 常量字段和局部变量不是变量,不能修改。 常量可以是数字、布尔值、字符串或 null 引用。 不要创建一个常量来表示你期望随时更改的信息。
const int X = 0;public const double GravitationalConstant = 6.673e-11;private const string ProductName = "Visual C#";常量表达式是在编译时可以完全计算的表达式。 因此,引用类型的常量的唯一可能值为字符串和 null 引用。
event
修饰事件的关键字。
public event MyEventHandler SomethingHappened;使用+=和-=来订阅或者取消事件
public class Subscriber{ public void HandleEvent(string msg) { Console.WriteLine($"收到消息: {msg}"); }}
class Program{ static void Main() { Publisher publisher = new Publisher(); Subscriber subscriber = new Subscriber();
// 订阅事件 publisher.SomethingHappened += subscriber.HandleEvent;
// 执行方法,触发事件 publisher.DoSomething();
// 取消订阅 publisher.SomethingHappened -= subscriber.HandleEvent; }}ref和out
ref
将指定参数的传值更改为传址,当我们在某处更改了这个ref修饰的变量,则全部的变量数据都会被修改。
using System.Linq;using System.Text;using System.Threading.Tasks;
namespace CsharpDemo{
class Program { static void Main(string[] args) { //宣告一隻雞 string chicken = "一隻雞"; Console.WriteLine("小時候 : " + chicken);
//呼叫成長方法 ChangAChicken(ref chicken);
Console.WriteLine("漂泊完回家後的雞 : " + chicken); Console.ReadKey(); } //宣告小雞長大的方法,這邊使用常數方法,不需要做return static void ChangAChicken(ref string newChicken) { //讓雞進化 newChicken = "孤獨" + newChicken; } }}out
Out修饰的变量可以返回多个值,但是在声明变量的时候Out变量无法被赋值
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;
namespace CsharpDemo{
class Program { static void Main(string[] args) { //宣告圓半徑 double x = 4; Console.WriteLine("圓半徑 : " + x);
//宣告圓周長 double y;
//呼叫計算方法 double area = GetArea(x, out y);
Console.WriteLine("圓周長 = " + y); Console.WriteLine("圓面積 = " + area); Console.ReadKey(); } //宣告方法來計算面積,要return 面積,out 圓周長 //out 变量让函数体内的变量的值,可以带到外面来 static double GetArea(double r, out double y) { //計算圓周長 y = 2 * r * Math.PI;
//計算圓面積 double s = (r * r) * Math.PI; return s; } } }override
override需要配合virtual函数,可以让子类修改父类的虚方法
// 要覆寫的基底類別
class Creature { public string name; public virtual string attack(Creature target) { return name + "攻擊了" + target.name; } }
// 子类继承修改新方法class Villager:Creature { public Villager(string name) { this.name = name; }
public override string attack(Creature target) { return name + "不能攻擊其他人"; } }readonly
只读修饰符,只能在构造函数中修改这个值,在参与程序的过程中,只读修饰的是无法修改的:
public class Program{ public class book { public int pages;
public book(int p) { this.pages = p; } }
public static readonly book a = new book(150); // 可以在初始化時、构造函数內指定數值
public static void Main() { // a = new book(200); // 不能修改 Console.WriteLine(a.pages); }}sealed
sealed应用于某个类后,可以阻止该类被其它类继承。sealed应用于方法后,不影响当前类的继承关系。它能够防止当前这个已经重写的方法被其子类去重写替换,也就是说sealed必须是与override搭配使用,对应虚方法或抽象方法。
public class A{ public virtual void Func() { Console.WriteLine("A"); }}
public class B : A{ public sealed override void Func() { Console.WriteLine("B"); }}
public class C : B{ //public override void Func()//编译器提示是密封的,无法重写。 //{ // Console.WriteLine("C"); //}}
class program { public static void Main() { A a = new A(); a.Func(); B b = new B(); b.Func();
}}static
static 成员在类被加载(或首次使用)时分配并常驻内存,因而无需 new 就能通过类名直接访问;所有对象共享同一份 static 成员(即同一块内存),对它的修改被所有实例看到。
using System;
public static class StaticClass{ public static int price; // 静态类只能包含静态成员 public static void GetPrice() { Console.WriteLine("StaticClass.GetPrice() 被调用"); }}
public class NotStaticClass{ public int price; // 普通实例字段
public void GetPrice() { Console.WriteLine("NotStaticClass.GetPrice() 被调用"); }
public static void GetPriceStatic() { Console.WriteLine("NotStaticClass.GetPriceStatic() 被调用"); }
private static int price00; // 静态字段,所有对象共享
// 用属性访问静态字段 public int price1 { get { return price00; } set { price00 = value; } }}
class Static_Program{ static void Main(string[] args) { // 静态类:直接访问,不用 new int staticClassPrice = StaticClass.price; Console.WriteLine("StaticClass.price 初始值:" + staticClassPrice);
// 普通类:必须 new NotStaticClass nStaticClass = new NotStaticClass(); int notStaticClassPrice = nStaticClass.price; Console.WriteLine("NotStaticClass.price 初始值:" + notStaticClassPrice); nStaticClass.GetPrice(); NotStaticClass.GetPriceStatic();
// 虽然宣告了不同的物件,静态变数仍然是共用的 NotStaticClass object1 = new NotStaticClass(); NotStaticClass object2 = new NotStaticClass();
object1.price1 = 10; // 把 object1 的 price1 设为 20 object2.price1 = 20; // 把 object2 的 price1 设为 10
Console.WriteLine("object1.price1 = " + object1.price1); Console.WriteLine("object2.price1 = " + object2.price1);
Console.ReadKey(); }}语句关键字
| 类别 | C# 关键字 |
|---|---|
| 选择语句 | if、switch |
| 迭代语句 | do、for、foreach、while |
| Jump 语句 | break、continue、goto、return |
| 异常处理语句 | throw、try-catch、try-finally、try-catch-finally |
| checked 和 unchecked 语句 | checked、unchecked |
| fixed 语句 | fixed |
| lock 语句 | lock |
| yield 语句 | yield |
语言特性
面向对象
C# 是面向对象的编程语言。 面向对象的编程的四个基本原则是:
- 抽象化 将实体的相关属性和交互建模为类,以定义系统的抽象表示形式。
- 封装 隐藏对象的内部状态和功能,仅允许通过一组公共函数进行访问。
- 继承 能够基于现有抽象创建新的抽象。
- 多态性 能够在多个抽象中以不同方式实现继承的属性或方法。
封装
封装将代码与其操作的数据绑定在一起,并保护它们免受外界干扰和滥用。封装是一个保护性容器,可以防止容器外部定义的其他代码访问代码和数据。
-
抽象:封装允许你在对象的内部工作和外部行为之间创建一个抽象层。这使得代码更容易理解,并降低了直接更改内部数据导致错误的风险。
-
模块化:封装允许你创建独立的对象,这些对象可以以不同的方式重用和组合。这使得你的代码更加模块化,更易于维护。
-
安全性:封装允许您控制对对象内部数据的访问,从而可以通过防止未经授权的访问或修改敏感数据来提高系统的安全性。
-
灵活性:封装允许你更改对象的内部实现,而不会影响系统的其余部分。这使得你的代码更加灵活,能够适应不断变化的需求。
-
可扩展性:封装允许您创建一个稳定而强大的系统,因为它允许您向对象添加新特性和功能,而不会影响现有代码。
在 C# 中,封装是通过使用访问修饰符(例如“private”、“protected”和“internal”)实现的。此外对于封装的数据项,需要用其他方法向外暴露。如一个在类中的私有变量,需要用属性来增加其访问方法。受保护的私有变量常常使用**_**作为变量的前缀。
namespace ToLearnObject{
class CAR { private int _speed;
public int Speed { // 访问器是用来控制对属性的访问的 get { return _speed; } // 设置器是用来控制对属性的赋值的 set { _speed = value; } } }
class Program { // myCar.Speed = 100; // 这里会被get访问器拦截,返回0。可以看出get访问器的逻辑 // myCar.speed = 10; // 直接访问私有字段会报错,需要通过属性访问
public static void Main() { CAR myCar = new CAR();
myCar.Speed = 120;
Console.WriteLine(myCar.Speed); }
}
}这样,速度变量的内部表示就对外界隐藏了,只能通过公共方法访问。这种封装提供了一定程度的抽象和对数据的控制。为什么需要封装这些数据,是为了系统稳定性。在程序编写过程和防御过程中,会有许多变量相同。一旦修改了一些不可以被修改的常量和变量会导致一些异常事件的发生,基本上很难溯源。OOP是为了提高系统可维护性、可扩展性、可重用性和灵活性。
抽象
对内表现为多个个体,对外表现为一个整体。抽象是一种将复杂化的逻辑整合成为一个出口,供他人使用。主要目的是为了让他人能快速上手。在编程之外,对于世界的物理状态,经过观察抽象成为一些公式变为数学语言,我们可以通过这些数学语言来理解世界。同样的,在构造一些函数的时候,抽象是为了让编写者观察这个程序所负责的功能,根据一些目的编写具体的变量以及函数,在通过这些来达成目的。
要叙述什么是抽象是很困难的,需求一直会变。就像我们一直不理解那些邪门的数学公式与定理,但是我们却能够使用,用这些达成目的。由这里延伸到其他的事件,C#中的方法也是经过抽象而来的,抽象不单单是指对函数,对类的一些抽象。抽象需要结合封装,如果外界能够轻易改变抽象完成的函数,类或者其他的什么,那么这个抽象是失败的。
回到C#中,通过将关键字 abstract 放在类定义之前,可以将类声明为抽象类。 例如:
public abstract class A{
}无法实例化抽象类。 抽象类的目的是提供多个派生类可以共享的基类的通用定义。 例如,类库可以定义一个抽象类,该类用作其许多函数的参数,并要求程序员使用该库通过创建派生类来提供类自己的实现。抽象类还可以定义抽象方法。 这是通过在方法的返回类型之前添加关键字 abstract 来实现的。 例如:
public abstract class A{ public abstract void DoWork(int i);}抽象方法没有实现,因此方法定义后面跟着分号,而不是带有实现代码的普通方法块。 抽象类的派生类必须实现所有抽象方法。 当抽象类从基类继承虚拟方法时,抽象类可以使用抽象方法替代虚拟方法。 例如:
// 虚拟方法类public class D{ public virtual void DoWork(int i) {
}}
public abstract class E : D{ // 可以将虚拟方法改写为抽象方法 public abstract override void DoWork(int i);}
public class F : E{ // 重写函数用override修饰 public override void DoWork(int i) {
}}多态
多态解决的是代码复用,也伴随着继承。在类体中,可以使用虚方法关键字,将需要多态的函数修饰。这个方法可以被继承的子类进行修改,修改方式是通过override修饰符号,来标注这个函数被重写了。
public class Shape{ // A few example members public int X { get; private set; } public int Y { get; private set; } public int Height { get; set; } public int Width { get; set; }
// Virtual method public virtual void Draw() { Console.WriteLine("Performing base class drawing tasks"); }}
public class Circle : Shape{ public override void Draw() { // Code to draw a circle... Console.WriteLine("Drawing a circle"); base.Draw(); }}public class Rectangle : Shape{ public override void Draw() { // Code to draw a rectangle... Console.WriteLine("Drawing a rectangle"); base.Draw(); }}public class Triangle : Shape{ public override void Draw() { // Code to draw a triangle... Console.WriteLine("Drawing a triangle"); base.Draw(); }}静态与动态
静态语言指的是代码变量在运行过程中其变量修饰符不会发生动态变化,其在编译的时候限定不会发生改变。C#就是典型的静态语言,
动态类型语言:在运行期间检查数据的类型的语言。用这类语言编程,不会给变量指定类型,而是在附值时得到数据类型。
静态类型语言的主要优点在于其结构非常规范,便于调试,方便类型安全;缺点是为此需要写更多的类型相关代码,导致不便于阅读、不清晰明了。动态类型 语言的优点在于方便阅读,不需要写非常多的类型相关的代码;缺点自然就是不方便调试,命名不规范时会造成读不懂,不利于理解等。
简单地说,在声明了一个变量之后,不能改变它的类型的语言,是静态语言;能够随时改变它的类型的语言,是动态语言。因为动态语言的特性,一般需要运行时虚拟机支持。
Lambda 表达式和匿名函数
Lambda表达式简化函数声明。常用于声明匿名函数,一个Lambda表达式声明的一个函数基本由如下构成
using System;using System.Linq;
class Program{ // 参数的最后一个是返回值 static void Main() { // 1. 基本用法:Func Func<int, int> square = x => x * x; Console.WriteLine($"square(5) = {square(5)}"); // 输出 25
// 2. 多参数 Func Func<int, int, int> add = (a, b) => a + b; Console.WriteLine($"add(3, 4) = {add(3, 4)}"); // 输出 7
// 3. 带语句块的 lambda Func<int, int, int> multiply = (a, b) => { int result = a * b; return result; }; Console.WriteLine($"multiply(3, 4) = {multiply(3, 4)}"); // 输出 12
// 4. Action 无返回值 Action<string> greet = name => Console.WriteLine($"Hello, {name}!"); greet("C#"); // 输出 Hello, C#!
// 5. Predicate 返回 bool Predicate<int> isEven = n => n % 2 == 0; Console.WriteLine($"isEven(10) = {isEven(10)}"); // 输出 True
// 6. 在 LINQ 中使用 int[] numbers = { 1, 2, 3, 4, 5 }; var evenNumbers = numbers.Where(n => n % 2 == 0);
Console.WriteLine("偶数有:"); foreach (var num in evenNumbers) { Console.WriteLine(num); // 输出 2, 4 } }}匿名函数也可以声明委托和事件类型: