语法经验
约 5429 字大约 18 分钟
2025-12-04
本篇的语法经验并非基础语法介绍,而是笔者个人对部分语法的使用经验以及使用时遇到的坑,希望能够在开发上帮助他人不踩相同的坑。愿世上再无Error
如果您对基础语法不太了解或根本没有概念的话,可以先阅读
基础介绍进行初步了解。在零基础的情况下 菜鸟教程 、 C语言中文网 都是最好的引路人,基础不免有些枯燥,但是在看懂程序的那一刻相信都是有成就感的。
1.1 属性和字段
属性 和 字段 作为两个比较相似的概念,在笔者初学时一直有所混淆。不会有人用半天都搞不清吧?原来是我啊哈哈
首先,在 属性 和 字段 之前,要先知道变量是什么。变量 是一个特定的存储单元,它拥有着自定义的变量名称,可设置的变量值。
public string test = "test string"如上便是一个变量,public 是变量的适用范围的声明,string 是变量类型,test 是变量名,"test string" 是变量值。
而这个变量也可以被称作字段,是一个 公共字段,那字段和属性的区别是什么呢?按照笔者的理解简单来讲,属性 是一个可以在调用时,进行具体数据处理的变量,字段 是一个只可以改变值的变量。
举个例子,下面是同一个含义的 字段 和 属性 ,都代表的是一个存放电话号码的方式。
字段:
public string phone = "+86 123456789000"属性:
private string? _phone;
public string? phone
{
// 可以省略,让属性作为只写
get
{
if (_phone == null)
return null;
return _phone + "+86";
}
// 可以省略,让属性作为只读
set
{
_phone = value;
}
}这么一看 属性 要多写很多代码,但实际上在使用时,可以通过传入的字符串自动给电话号码添加前缀。而 get 中的处理完全可以根据需求进行编写,使用上来说,相比单一的 字段 赋值,属性 可以有更多的数据处理,可以让变量的处理更加统一。同样的 set 也可以自行数据处理。
一般来说 属性 不会单独出现,而是存在于实体类中,如下的用户实体类
public class UserInfo
{
public string username {get; set;}
private string? _phone;
public string? phone
{
get
{
if (_phone == null)
return null;
return _phone + "+86";
}
set
{
_phone = value;
}
}
public string nickname {get; set;}
}通过 username、phone、nickname 描述了 UserInfo 的属性,在使用时可以对每个 属性 赋值进行使用。区别于 字段 的赋值,实体类中的 属性 可以进行单独修改更加多样化。
就笔者个人使用体验而言,对于有着固定结构的数据,属性 可以非常简便的实现数据结构的复用。而 字段 一般会用于比较单一的变量,虽然字典类型也可以处理复杂的变量,但笔者个人认为带有 属性 的实体类可以更好的统一管理和扩展。
1.2 反射
反射 (Reflection) ,官方解释是命名空间中的 System.Reflection 类以及 System.Type 可用于获取有关加载的程序集及其中定义的类型的信息,例如类、接口和值类型(即结构和枚举)。 还可以使用 反射 在运行时创建类型实例,以及调用和访问它们。
笔者个人经验而言,可以简单的理解为 反射 可以认为是在程序中调用外部文件以及文件内的程序。非常典型的来说,现有dll文件,如果需要使用内部的程序、方法或函数,那便可以使用反射来调用(虽然还是不怎么用)。
是否会觉得用 反射 调用dll的方法有些多此一举,明明可以直接用添加项目引用功能直接添加dll文件,编写反射反而还变得麻烦了。其实 反射 的作用是为了动态调用,只需要将 反射 进行初步封装,就可以根据传入的参数调用指定的dll文件和方法。
类库DLL:
// dll内程序
namespace TestClassLibrary
{
public class TestClass
{
public static void test(string input)
{
Console.WriteLine(input);
}
}
}项目调用:
using System.Reflection;
namespace TestApp
{
internal class Program
{
static void Main(string[] args)
{
string DllPath = @"your_dll_path";
// 加载dll文件
Assembly asm = Assembly.LoadFrom(DllPath);
// 获取类名,必须使用 命名空间+类名
Type t = asm.GetType("TestClassLibrary.TestClass");
// 实例化类
object o = Activator.CreateInstance(t);
// 获取指定方法
MethodInfo method = t.GetMethod("test");
// 入参参数
object[] obj =
{
"Hello world!"
};
// 对方法进行调用
var keyData = method.Invoke(o, obj);
}
}
}上述是一个简易的反射调用例子,在运行 TestApp 项目后,就会弹出控制台打印出 "Hello world!",这样就可以简易地调用其他的dll文件,稍作封装就可以实现动态调用dll库了。虽然动态调用非常便利,但由于性能问题,笔者在此不建议频繁使用拖垮整个程序的运行效率。
1.3 委托和事件
1.3.1 委托
委托 (Delegate),是 C# 中比较特殊的语法,在各种地方都很常用。笼统点讲,委托 是一种类型安全的函数指针,它允许将方法作为参数传递给其他方法。如果需要详细了解可以查阅 微软官方委托简介。
委托 是一个统称,具体上来说分为一下四类 可以在自己的环境中调试运行熟悉一下:
- delegate 普通委托,可以无返回值也可以指定返回值类型
// 定义委托
public delegate int TestDelegate(int x, int y);
// 定义被调用方法
public static int Add(int x, int y)
{
return x + y;
}
static void Main(string[] args)
{
// 实例化委托
TestDelegate test_delegate = new TestDelegate(Add);
// 传入的入参调用Add方法后返回
Console.WriteLine(test_delegate(4, 6));
}- action 无返回的泛型委托
// 定义被调用方法
public static void OutputString(string input)
{
input = "input: "+ input;
Console.WriteLine(input);
}
// 将方法作为参数传递
public static void TestAction<T>(Action<T> action, T obj)
{
action(obj);
}
static void Main(string[] args)
{
// 设定泛型为string
TestAction<string>(OutputString, "Hello World");
}- func 必须有返回值的泛型委托
// 定义被调用方法
private static int Add(int a, int b)
{
return a + b;
}
// 将方法作为参数
public static int TestFunc<T1, T2>(Func<T1, T2, int> func, T1 a, T2 b)
{
return func(a, b);
}
static void Main(string[] args)
{
int result = TestFunc<int, int>(Add, 4, 6);
Console.WriteLine("result=" + result);
}- predicate 只接受一个入参且返回bool的委托
// 定义被调用的方法
private static bool CompareXY(Point obj)
{
if (obj.X > obj.Y)
return true;
else
return false;
}
static void Main(string[] args)
{
Point[] points = {
new Point(1,2),
new Point(2,1),
new Point(1,1),
new Point(2,2)
};
// 将CompareXY作为参数传入
// Array.Find()的第二个参数为predicate
Point first = Array.Find(points, CompareXY);
}简单来说,上面四个都是将 方法 作为 入参 传入委托进行调用,不同的是对 传入方法 的要求。可以通过传入相同结构的不同方法,实现更加复用性的代码,举个例子,如果需要对两个数字进行运算操作,只需要定义好对应的被调用方法,在需要的地方进行委托调用即可动态实现运算。而独立出来的被调用方法就可以统一进行扩展管理。
1.3.2 事件
事件(Event),笼统来说是用于将特定的事件通知发送给订阅者。事件通常用于实现观察者模式,它允许一个对象将状态的变化通知其他对象,而不需要知道这些对象的细节。
对笔者而言,最为熟悉的 事件 就是在 winform 项目中的,winform 就是windows窗体程序(不清楚的可以自行查阅一下,理解成正常使用的桌面程序即可)。在 winform 中时常会用到鼠标点下时触发事件、键盘按下时触发事件等。除去 winform 项目,事件也可用于其他项目。如果需要详细了解可以查阅 微软官方事件简介。
此处代码引用 菜鸟教程 的实例代码:
// 定义一个委托类型,用于事件处理程序
public delegate void NotifyEventHandler(object sender, EventArgs e);
// 发布者类
public class ProcessBusinessLogic
{
// 声明事件
public event NotifyEventHandler ProcessCompleted;
// 触发事件的方法
protected virtual void OnProcessCompleted(EventArgs e)
{
ProcessCompleted?.Invoke(this, e);
}
// 模拟业务逻辑过程并触发事件
public void StartProcess()
{
Console.WriteLine("Process Started!");
// 这里可以加入实际的业务逻辑
// 业务逻辑完成,触发事件
OnProcessCompleted(EventArgs.Empty);
}
}
// 订阅者类
public class EventSubscriber
{
public void Subscribe(ProcessBusinessLogic process)
{
// 注册实际调用的事件方法
process.ProcessCompleted += Process_ProcessCompleted;
}
// 被调用方法
private void Process_ProcessCompleted(object sender, EventArgs e)
{
Console.WriteLine("Process Completed!");
}
}
internal class Program
{
static void Main(string[] args)
{
ProcessBusinessLogic process = new ProcessBusinessLogic();
EventSubscriber subscriber = new EventSubscriber();
// 订阅事件
subscriber.Subscribe(process);
// 启动过程
process.StartProcess();
Console.ReadLine();
}
}上述示例简单实现了一个简单的发布和订阅,上述的注释已经比较完善可以先自行理解一下。发布者类 中的声明事件,就是通过实例化一个委托用于接受实际需要调用的方法。在 订阅者类 中通过给委托注册实际事件,在 OnProcessCompleted() 时就会自动触发。
还是用 winform 项目的逻辑进行解释,在属性中设置的鼠标点下时触发事件就像是提前定义的 委托,再给该 事件 添加方法时,就相当于是注册了对应的调用方法,在实际触发鼠标点下的事件时就会自动调用指定方法。
1.3.3 委托和事件的区别
这个问题几乎是八股文里的常客,在刚刚了解了 委托 和 事件 后,是否对这两个语法有了一定的了解。在笔者的经验里 事件 是 委托 的封装应用,通过对 委托 的封装提高了安全性,同时提供了发布-订阅的模式。使用上也能感受到,每次注册事件其实就是对委托进行方法的传入,和使用普通委托区别并不大。
1.4 泛型和Object
1.4.1 泛型
泛型 (Generic),是各种语言中比较常见的一个语法,在 C# 中时常会遇到 参数类型是 T 的情况。而 T 就是一般用于代指 泛型 的字母(其实也可以自定义,但后续为方便,都以 T 作为代指),此处列出标记符仅供参考。
泛型标记符:
- E Element集合元素
- T Type 实体类
- K Key 键
- V Value 值
- N Number 数值类型
- ? 表示不确定的C#类型
其实 A-Z都可以作为 泛型 标记符,上面只是一种约定,增强代码的可读性,方便团队间的合作开发。
在 微软泛型类型概述 中也对 泛型 有所描述, 泛型 是具有占位符(类型参数)的类、结构、接口和方法,用于存储或使用的一个或多个类型。 泛型 集合类可能使用类型参数作为它存储的对象类型的占位符。类型参数显示为其字段的类型及其方法的参数类型。 泛型 方法可能将其类型参数用作其返回值的类型或其正式参数之一的类型。
笔者个人的理解比较通俗,泛型 可以看作是一个容器,在实例化或定义时,就相当于是给容器倒水、饮料等各种各样的液体。在确定了容器内的类型后,返回也将会是该类型。不会出现刘谦变魔术呢样的! 在之前 委托 的示例代码中,也有使用到 泛型 ,目的也是为了让代码更为复用和便利。
public void SwapVariable<T>(ref T input_a, ref T input_b)
{
T temp = input_a;
input_a = input_b;
input_b = temp;
}上述代码就是一个简便的 泛型 例子,通过传入参数,将两个参数进行交换。而 T 便是传入的类型。如果能理解的话,可以看看之前 委托 的示例代码,代码中该代码中的 TestAction<T> 就在使用时被定义为 string 类型。
1.4.2 泛型和Object
说到这个题目时,您会不会好奇,既然已经有了 object 这个便利的万能类型,那为什么还需要有 泛型 的存在呢?总的来说,是因为 安全性。
object 作为万能类型,是所有类型的父类,可以强制转换成任意类型。
object o1 = 123;
int x = (int)o1;
object o2 = "hello";
string y = (string)o2;从上面的代码,可以很清晰的看到在赋值给对应 变量 时,需要进行强制转换而这个过程就是简易的 装箱 和 拆箱 。int 类型的123先被装箱成为 object 类型的o1,在赋值时 object 被 拆箱 赋值为y。这个过程并不安全,而且每次都需要手动强制转换也非常麻烦。
而相较于 object 的手动强制转换,泛型 并不需要 装箱拆箱 操作,在明确类型的情况下,可以让代码更加复用更加安全。所以总的来说,泛型 非常适合用于已确定类型时提高代码复用性,而 object 适合用于不确定类型,如反射等情况下,手动强制转换来确定变量类型的情况。
1.4.3 补充
上述的两小节对 泛型 和 object 都描述,除去这两个之外,还有另外两个比较常用的语法,分别是 var 和 dynamic。 此处引用几篇文章用于分辨, dynamic的正确用法 , dynamic、var、object和泛型。笔者个人习惯的话,一般在确定需要使用的类型情况下,就直接使用确定的类型,不去使用 var 、 dynamic 、 object 。在不确定的情况下,方法的参数类型会使用 泛型 让方法变得更加复用。 而 dynamic 则是在需要动态分析变量类型的情况下才会用到。至于 object 则是除非不得不用,不然一般用不到 object 来进行拆箱装箱操作。
1.5 Linq
1.5.1 语言集成查询
语言集成查询(Linq),是一种统一查询的语法,Linq 集成在C#中,可以使用相同的基本编码模式来查询和转换 XML 文档、SQL 数据库、.NET 集合和其他任何格式中的数据。很大程度上给查询的语法提供了遍历。
比如一个很简单的例子,现有一个 List 存放数据,需要从中判断对应字符串长度大于3的元素数量,就可以用 Linq 快速实现。
List<string> elementList = new List<string>()
{
"aaaaa",
"bb",
"cccc",
"ddd"
};
int target_count = elementList.Where(t => t.Length > 3).Count();target_count 的值就是通过 Linq 语句查询出的元素数量,而 Where 以及没有使用到的 Select 、 OrderBy 、 Sort 等,都是 Linq 语句。就笔者个人而言,Linq 的使用方式其实和 SQL 语句差不多太多,都可以通过自定义的逻辑进行查询,并且还可以通过对应的语句进行便捷查询。
select count(*) from elementList where length > 3对照上面的 Linq 语句和 SQL 语句,是否能感受到使用上的相似。 说明学好linq就会sql了!(划掉)
1.5.2 Linq的拆解
刚刚有介绍 Linq 的基础用法,这里将对每个部分进行拆解解释。
// 用户实体类
public class User
{
public string name {get; set;}
public int age {get; set;}
public string sex {get; set;}
public string phone {get; set;}
}
// 样本数据
List<User> userList = new List<User>()
{
new User { name = "Test1", age = 18, sex = "male", phone = "123" },
new User { name = "Test2", age = 22, sex = "female", phone = "234" },
new User { name = "Test3", age = 20, sex = "male", phone = "456" },
new User { name = "Test4", age = 25, sex = "female", phone = "567" },
new User { name = "Test5", age = 19, sex = "male", phone = "678" },
};
List<User> target_user = userList
.Where(t => t.sex.ToLower() == "male")
.OrderBy(t => t.age)
.ToList();List<User>是User的列表,也可以说是样本数据源。userList.Where()就是对数据源进行条件筛选操作,同理OrderBy和ToList也是对数据源进行的操作。(t => code)括号内的t代表循环项类似foreach循环。t.sex.ToLower()是对循环项的属性进行处理,可以通过属性和属性的内置方法对属性进行预先处理。t.sex.ToLower() == "male"代表的是条件判断,和正常的if语句一样,也可以使用逻辑运算符进行多条件判断。
上述对具体的 Linq 语句进行拆解,总的来说,Linq 提供了一种统一的语句对指定类型的数据源进行查询和筛选。在 C# 中是非常好用的一种语法,常见于筛选和判断数据集内指定条件的数据。
1.6 自动化
1.6.1 UI自动化概述
UI自动化 解释起来就是字面意思,自动化进行UI的交互。在此处要进行类别的区分,UI分为 Web UI 和 窗口UI,此处进行 窗口UI 的自动化操作分享, 窗口UI 基本建立在 windows 系统中,本段后续使用的也是 windows。
在 微软官网 中对 UI自动化 的概述如此,Microsoft UI 自动化是适用于 Microsoft Windows 的新可访问性框架,可在支持 Windows Presentation Foundation(WPF)的所有操作系统上使用。UI自动化提供对桌面上大多数用户界面(UI)元素的编程访问,使屏幕阅读器等辅助技术产品能够向最终用户提供有关UI的信息,并通过非标准输入方式操控UI。 UI自动化还允许自动测试脚本与UI交互。
看着非常官方,对笔者而言最先接触到的 UI自动化 是按键精灵,自动控制键盘与鼠标进行操作。而 C# 中的 UI自动化 就可以看成是升级版,能够更加深度进行自动化操作和控制。在提到 C# 的 UI自动化 ,就不得不说到几样东西了,UIAutomation 、 inspect.exe 、 win32 API。下面将会对此简述。
UIAutomation是C#中的一个非常强大的自动化库,提供了API可以快速进行窗口UI的自动化。inspect.exe该工具是一个图形用户界面 (GUI) 应用程序,可用于收集用于提供程序和客户端开发和调试的 UI 自动化信息,它包含在 Windows SDK 中。win32 API(应用程序编程接口)是用于开发 Windows 操作系统应用程序的核心接口,其中包含很多底层的系统操作。
1.6.2 UIAutomation
UIAutomation 是 .net framework 提供的一个自动化框架,用于访问、控制、测试 windows 桌面应用程序的UI元素。它遵循 Microsoft UI Automation (UIA) 规范,是 windows 官方的UI自动化技术。
笔者自己在使用这个库的情况,一般是用于工作中的一些自动化测试和自己兴趣上的一些自动化脚本,一下举一个比较简单的例子。
using System.Windows.Automation;
// 获取根节点
var root = AutomationElement.RootElement;
// 查找计算器窗口
var condition = new PropertyCondition(AutomationElement.NameProperty, "计算器");
var calcWindow = root.FindFirst(TreeScope.Children, condition);
// 查找按钮(例如“7”)
var buttonCond = new PropertyCondition(AutomationElement.NameProperty, "7");
var button_7 = calcWindow.FindFirst(TreeScope.Descendants, buttonCond);
// 点击按钮
var invoke = button_7.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
invoke.Invoke();分部解析:
var root = AutomationElement.RootElement;AutomationElement.RootElement 是根节点,可以理解为 html 中的顶级 <html> 标签,更通俗一点可以理解为整个桌面。对于代码来说,所有的窗口和控件,都是在 RootElement 下的子级。
var condition = new PropertyCondition(AutomationElement.NameProperty, "计算器");创建搜索条件,搜索 AutomationElement.NameProperty 为 计算器 的元素, AutomationElement.NameProperty 一般是指窗口或元素的标题。
var calcWindow = root.FindFirst(TreeScope.Children, condition);搜索到 计算器 的窗口,从根节点开始搜索,以 TreeScope.Children 的模式进行搜索,搜索条件为 condition。 TreeScope 是UI自动化中的枚举,其中包含 Chlidren 表示搜索直接子级, FindFirt() 获取第一个找到的窗口。
Ancestors指定搜索包括元素的上级(包括父级)。不支持作为条件。Children指定搜索包括元素的直接子级。Descendants指定搜索包括元素的子代(包括子级)。Element指定搜索包括元素本身。Parent指定搜索包括元素的父级。不支持作为条件。Subtree指定搜索包括搜索的根和全部子代。
var buttonCond = new PropertyCondition(AutomationElement.NameProperty, "7");
var button_7 = calcWindow.FindFirst(TreeScope.Descendants, buttonCond);该部分的搜索也同上,但是搜索到的 Element 元素是 Button ,在此处就是计算机的数字7按钮。
var invoke = button_7.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
invoke.Invoke();转换成可点击的控件模式, invoke.Invoke() 模拟点击按钮。
总体逻辑上来说,使用 UIAutomation 需要先从根节点一层层向下找到对应的窗口或控件进行操作使用。 RootElement 为所有元素的根节点,通过其找到对应窗口,然后以窗口作为根节点,进行具体元素的查找。在使用时,如果可以确定元素的路径不会变更,那也可以将路径保存后直接调用元素,省去每次都查找的时间。此处也可以通过上述有提到的 inspect.exe 进行图形化的获取。
inspect.exe使用
inspect.exe 是微软的辅助功能工具,是 Windows SDK 中的小工具。微软官方介绍 中也对 inspect.exe 有着初步介绍。
笔者个人经验,待待补充。
更新日志
403d4-修改 Github 工作流的配置,以方便未来支持前后端拓展于