Lazy loaded image
📘Csharp学习笔记
字数 17702阅读时长 45 分钟
2024-9-12
2026-9-15
😀
此笔记是C#学习笔记,个人认为对入门面向对象思想 和 了解函数栈帧有很大的帮助 视频并没有看完,只是简单的做了个课设(串口助手) 以后有机会再往后看吧~

一、类与名称空间

1. 简介

  • 类(class)构成程序的主题
  • 空间名称(namespace)以树型结构组织类(和其他类型)
    • 例如Button 和 Path 类。
  • using 类似于 C语言的引用头文件,在C#中是名称空间。
 
使用名称空间,可以避免类的冲突。 比如Windows名称空间 与 IO 名称空间中都有Path 类。他们的作用不同但名称相同。这时候如果要使用就需要用 全限命名 xxx.xx.path
所以在以后使用时,就要对类进行精确命名,并且应该把他精确的放到 名称空间中去

2. 类库的引用

类库

  • 是以.dll为结尾的文件。 dll为(dynamic link Library) 动态 链接 库。在项目树的references可查看
  • 在引用类库的时候,要尤其注意依赖关系,最好用依赖关系比较弱的包。在类库有问题时,可以暂时用别的类库先替代。

类库的引用

  • 类库可以有黑盒引用,也可以有白盒引用
      1. 黑盒引用是对编译好的dll文件直接引用
      1. 白盒引用是把源代码放到项目中直接引语
在生成项目的时候,系统会自动引用一些必要的或者常用的库。可以再次选择系统自带的库,也可以去引用别人或是自己曾经写的库

管理NuGet包

  • 在使用类库时,往往还需要更底层的类库。如果手动添加会比较麻烦。我们可以安装Entity Framework ,安装后,我们就不需要担心类库的依赖了。NuGet会自动管理依赖库。

白盒引用

朋友发给你他的项目,你可以把他的项目添加到你的解决方案(Solution)中。
添加后就可以直接使用这个项目中的名称空间中的类
在解决方案中添加现有项目。然后在主项目中引用添加的项目

编译dll

我们要发给别人dll时,需要创建醒目为Class Library。同样也可以在自己的项目中引用

3. 依赖关系

  • 类(或对象)之间的耦合关系
  • 好的程序追求“高内聚,低耦合”
    • 高内聚指的是 类和名称空间精准的分类放置
    • 依赖关系,也被称为耦合关系,低耦合指的就是依赖关系低
  • UML(通用建模语言)类图
    • 类图用来展现类和类之间的关系。

二、类、对象、类成员、实例成员

类(class)

类(class)是现实世界对实物的模型
  • 类是对现实世界事物进行抽象所得到的结果
    • 事物包括“物质"( 实体 )与“运动"( 逻辑)
    • 建模是一个去伪存真、由表及里的过程
    • (去伪存真的意思是,在程序中去除用不到的功能)(表是我们最终达到的效果。里则是程序内封装的逻辑)

类(class)与对象(instance)的关系

  • 对象也叫实例(instance),是类经过“实例化”后得到的内存中的实体
    • Formally"instance"is synonymous with“object”——对象和实例是一回事
    • “飞机"与”一架飞机"有何区别?天上有(一架)飞机——必需是实例飞,概念是不能飞的
    • 有些类是不能实例化的,比如“数学"( Math class),我们不能说”一个数学
  • 依照类(class),我们可以创建对象,这就是“实例化“
    • 现实世界中常称“对象", 程序世界中常称“实例”
    • 二者并无太大区别,常常混用,初学者不必迷惑
  • 使用new操作符创建类的实例
  • 引用变量与实例的关系
    • 孩子与气球
    • 气球不一定有孩子牵着
    • 多个孩子可以使用各自的绳子牵着同一个气球,也可以都通过一根绳子牵着气球
  • 在现实世界中一般称作对象、在程序世界中一般称作实例
  • 以Form来举例
    • 使用new来创建实例。
    • 使用Form MyForm1;来创建引用变量(可以理解为C语言指针)
    • 使用MyForm1 = new Form();来将引用变量绑定到实例上(可以理解为指针指向实例)
    • 不同于C,需要解引用。 我们后续对MyForm1的操作,实际上是对new Form() 这个创建的实例在操作

类的三大成员

  • 属性(Property)
    • 存储数据”,组合起来表示类或对象当前的状态
  • 方法(Method)
    • 由C语言中的函数(function)进化而来,表示类或对象”能做什么’
    • 工作中90%的时间是在与方法打交道,因为它是“真正做事”、”构成逻辑”的成员
  • 事件(Event )
    • 类或对象通知其它类或对象的机制,为C#所特有(Java通过其它办法实现这个机制)
    • 善用事件机制非常重要
  • 使用MSDN文档
  • 某些特殊类或对象在成员方面侧重点不同
  • 模型类或对象重在属性,如Entity Framework
  • 工具类或对象重在方法,如Math,Console
  • 通知类或对象重在事件,如各种Timer

静态成员与实例成员*

静态成员属于类本身而不是类的特定实例。在创建时可以用static 来前缀

一、语义及特点

静态成员
  1. 静态成员在语义上表示它是 “类的成员”,可想象成班级里的公共用品如时钟,也像人类的总数、平均身高体重等概念。它是与生俱来的,只要类被抽象出来,静态成员就存在。
  1. 静态成员隶属于类本身,在内存中只有一个副本。例如 Console.WriteLine 中的 WriteLine 是静态方法,通过类名 Console 直接访问,无需特定对象。
  1. 静态成员可以被所有的对象访问,而不需要通过特定的对象去访问。 同时,静态成员只能访问其他静态成员,不能访问类的实例成员。
实例成员
  1. 实例成员在语义上表示它是 “对象的成员”,如同学生自己的书包,每个学生(对象)都有自己独立的书包,不同学生书包里的东西可能不一样。
  1. 实例成员属于特定对象的实例,每个对象都有自己的实例成员副本。
  1. 实例成员需要通过实例化对象来访问, 并且实例成员可以访问类的其他实例和静态成员。

二、访问方式

  1. 静态成员访问
静态成员可以通过类名直接访问,如 Console.WriteLine 。
  1. 实例成员访问
实例成员需要通过实例化对象来访问,如From from = new From();。 这里的意思就是用From这个类。实例化了一个From对象。
当使用类名From来引出方法和工具时,通常是调用静态方法或访问静态成员。
  • 静态方法和成员属于类本身,在内存中只有一份,不依赖于特定的对象实例。
  • 可以直接通过类名来访问,无需创建对象实例。例如From.StaticMethod();
  • 静态成员不能直接访问类的实例成员,因为它们与特定的对象实例无关。
使用实例名来引出方法和工具时,是调用实例方法或访问实例成员。
  • 实例方法和成员属于特定的对象实例,每个对象实例都有自己独立的副本。
  • 必须先创建类的实例,然后通过实例名来访问。例如from.InstanceMethod();
  • 实例成员可以访问类的其他实例成员和静态成员,因为它们在对象的上下文中运行。
总的来说,主要区别在于静态成员和实例成员的本质特性以及访问方式不同。静态成员适用于与类相关的通用操作,不依赖于特定的对象状态;而实例成员则与特定的对象实例相关联,反映对象的具体状态和行为。

三、基本元素、初识类型、变量与方法、算法简介

1. 章节介绍

  • 成C#语言的基本元素
    • 标记( Token ) 标记 是对于编译器有意义的记号
      • 关键字(Keyword)
      • 操作符(Operator)
      • 标识符(ldentifier)
      • 标点符号
      • 文本
    • 注释与空白
  • 简要介绍类型、变量与方法算法简介

2. 构成C#语言的基本结构

一、标记(token)概述
在 C# 中,标记是对编译器有意义的记号,主要分为以下几类:
  1. 关键字:具有特定含义的保留词汇,如for循环中的 “for” 就是关键字。它们在语言中被赋予了特定的功能和用途,用于构建程序的逻辑结构和表达特定的操作。
  1. 操作符:用于执行各种运算,如算术运算(+、-、*、/ 等)、比较运算(==、!=、>、< 等)、逻辑运算(&&、||、! 等)。
  1. 标识符
      • “什么是合法的标识符” 部分主要涉及到在 C# 中定义变量、方法、类等命名时需要遵循的规则。合法的标识符通常由字母、数字和下划线组成,但不能以数字开头。并且要不与C#的保留字
      • “大小写规范” 则涉及到 C# 中标识符的大小写敏感性。一般来说,C# 是区分大小写的语言。
      • “命名规范” 包括各种命名约定,如变量名通常使用小写字母开头的驼峰命名法(myVariable),类名使用大写字母开头的帕斯卡命名法(MyVarable)等。
        • (一般变量名,都是驼峰法。类、名称空间、等都是实用的帕斯卡法)
  1. 标点符号:在 C# 中,标点符号用于分隔语句、表示语句的结束等,例如分号(;)用于结束一条语句,括号(())用于方法调用和表达式分组等。
  1. 文本(字面值)
      • “整数” 可以有多种后缀,例如 “L” 表示 long 类型的整数,“U” 表示 unsigned 整数等2、2L。
      • “实数” 也有多种后缀,用于表示不同精度的浮点数。F单精度,D双精度,默认双精度。
      • “字符字符串” 用于表示一串字符,用双引号括起来。
      • “布尔” 类型只有两个值:true 和 false。
      • “空(null)” 表示一个对象没有值。
二、注释与空白
  1. 单行注释:使用 “//” 开头,用于在代码中添加单行注释,解释代码的功能或提供其他信息。
  1. 多行(块注释):使用 “/” 和 “/” 包围起来,可以包含多行注释内容。多行注释通常用于对一段代码或一个函数进行详细的解释。

3. 初识类型、变量和方法

  • 初识类型 (Type)
    • 亦称数据类型 (Data Type)。不同的数据类型在内存中占据不同的空间大小,并且具有特定的操作和用途。例如,整数类型用于存储整数值,浮点类型用于存储带有小数部分的数值。
  • 变量是存放数据的地方,简称 “数据”
    • 变量的声明:在 C# 中,变量的声明需要指定变量的类型和名称。例如,int num;声明了一个名为num的整数类型变量。
    • 变量的使用:声明变量后,可以通过赋值语句为变量赋予具体的值。例如,num = 10;将整数值 10 赋给变量num。变量可以在表达式中使用,也可以作为方法的参数传递。
  • 方法 (旧称函数) 是处理数据的逻辑,又称 “算法”
    • 方法的声明:方法的声明包括方法的返回类型、方法名、参数列表和方法体。例如,public int Add(int a, int b) { int result = a + b; return result; }声明了一个名为Add的方法,该方法接受两个整数参数ab,并返回它们的和。其中public 是 为了让 类 外边 也能访问到这个方法。
    • 方法的调用:可以通过方法名和参数列表来调用方法。但在此之前 要把类创建为实例。然后再使用int sum = c.Add(5, 3);调用了Add方法,并将返回值赋给变量sum。(其中c为创建的实例名)
    • 方法一般分为三类:有输入有输出,例如求和;没输入有输出,例如显示日期;无输入有输出。一般是在方法内直接输出或者做了某些事情。
  • 程序 = 数据 + 算法
    • 有了变量和方法就可以写有意义的程序了。程序通过对数据的操作和处理来实现特定的功能。变量存储程序中的数据,方法提供对数据的处理逻辑。通过合理地组织变量和方法,可以构建出复杂的程序。一般有for 和 递归。
在 C# 中,正确地理解和使用数据类型、变量和方法是编写高效、可靠程序的基础。了解不同数据类型的特点和用途,合理地声明和使用变量,以及设计有效的方法,可以提高程序的可读性、可维护性和性能。

四、详解C#类型、变量与对象

什么是类型(Type)

一、又名数据类型( Data Type )
  • A data type is a homogeneous collection of values, effectively presented, equipped with a set of operations which manipulate these values
  • 数据类型是值的同质集合,有效地呈现,配备了一组操作这些值的操作
  • 是数据在内存中存储时的“型号(Type 又称型号)
  • 若误小内存容纳大尺寸数据会丢失精确度、发生
  • 大内存容纳小尺寸数据会导致浪费
  • 编程语言的数据类型与数据的数据类型不完全相同
二、强类型语言与弱类型语言的比较
数据被数据类型约束,叫强类型语言。反之被约束的少,甚至不约束。叫弱类型
  • C语言示例:if条件
    • 例如在C语言中 if(x = 10)是可以被编译过去的。 而 C#语言则不允许, 因为C#语言中 括号内强制 要求为一个bool类型的值,也就是只能为true 和 false
  • JavaScript示例:动态类型
    • var myVar = 100; myVar = “hello”; alert(myVar); 这段代码在这里是可以运行的
  • C#语言对弱类型/动态类型的模仿
    • 在C#语言中为了模仿弱类型,引用了一个新的关键字:dynamic 这个关键字可以实现上边的功能

在C#语言中的作用

一、程序的静态与动态时期
程序在执行时为动态时期(运行时期),也称为运行或调试阶段。在这个阶段,程序被加载到内存中运行。可以说,动态的程序是在内存里的,此时方法调用使用栈(Stack),存储对象使用堆(Heap)。
而在静态时期,即写代码或编译阶段,程序以代码文件的形式存储在硬盘里。
二、栈(Stack)的特点
栈的空间相对较小,一般为 1 - 2M,但其速度快。当方法被调用时,方法的参数和局部变量会在栈上分配内存。由于栈的内存空间有限,当不断进行方法调用或在方法中不断创建新的变量,导致栈满时,就会出现栈溢出(Stack overflow)的情况。例如,可以通过直接递归某个方法,在方法中不断创建新的变量来测试栈溢出;或者在方法前添加unsafe前缀后,使用int* p = stackalloc int(9999999);在栈中切除很大的一块内存,也可能引发栈溢出。
三、堆(Heap)的特点
堆的空间相对较大,能有几个 G。对象存储在堆上,在堆上分配内存相对较慢。同时,堆可能会出现内存泄漏的情况,即如果程序员忘记释放不再使用的内存,就会导致内存浪费。不过,C# 有垃圾收集器的机制,垃圾收集器会自动收集不再使用的对象所占用的内存空间,从而在一定程度上避免了内存泄漏的问题。
 
在C#语言中的作用
  • 一个C#类型中所包含的信息有:
    • 存储此类型变量所需的内存空间大小
    • 此类型的值可表示的最大、最小值范围
    • 此类型所包含的成员(如方法、属性、事件等 )
    • 此类型由何基类派生而来
    • 程序运行的时候,此类型的变量在分配在内存的什么位置
      • Stack简介
      • Stack overflow
      • Heap简介
      • 使用Performance Monitor查看进程的堆内存使用量
      • 关于内存泄漏
    • 此类型所允许的操作(运算)

C#语言的类型系统

一、C#的五大数据类型:
  • 类(Classes): 如Windows, Form, Console, String
  • 结构体(Structures): 如Int32, Int64, Single, Double
  • 枚举(Enumerations): 如HorizontalAlignment, Visibility
  • 接口(Interfaces )
  • 委托(Delegates )
二、C#类型的派生谱系:
notion image
C shap中 的类型分为两种:引用类型和值类型。引用类型分为类、接口、委托;值类型有结构体和枚举。所有类型都以Object类型为自己的基类型。
第一组(左边)
  • Object、string 为真正的数据类型(因为这两个太常用了。所以C#把他吸收为自己的关键字了。) string 为 引用类型 存储在 堆
  • 横线下边的三个数据类型 不是具体的数据类型,而是我们用他们三个关键字去定义自己的数据类型
第二组
  • 同上
第三组
  • 上为bool类型的值
  • 中为 无返回值 和 为空
  • 下为 声明变量

变量、对象与内存

  • 什么是变量
      1. 表面上来看,变量的用途是存储数据
      1. 实际上,变量表示了存储位置,并且每个变量都有一个类型,以决定什么样的值能够存入变量 (变量名(对应这)变量的值在内存中存储位置 并且决定了什么样的值能够存融入)
      1. 变量一共有7种·
          • 静态变量(静态字段),实例变量(成员变量,字段), 数组元素, 值参数, 引用参数, 输出参数, 局部变量 ref 引用的 值参数 为引用变量。out 引用的 值参数 为输出参数 一般称的变量为:局部变量
      1. 狭义的变量指局部变量,因为其它种类的变量都有自己的约定名称
          • 简单地讲,局部变量就是方法体(函数体)里声明的变量
      1. 变量的声明
          • 有效的修饰符组合opt - 类型 - 变量名 - 初始化器opt。 opt意思就是可有可无 比如public static int Age = 0:; public static 是有效的修饰符组合 int 为类型 Age 为变量名 =0 则为初始化器
          总结: ”变量 = 以变量名所对应的内存地址为起点、以其数据类型所要求的存储空间为长度的一块内存区域“ (大小端 与硬件有关) 值类型在栈上分配内存,不同的值类型占用的字节大小固定,如byte占用 1 个字节,int占用 4 个字节等。 引用类型的对象在堆上分配内存,引用本身在 32 位系统上通常占用 4 个字节,在 64 位系统上通常占用 8 个字节,它只是指向堆上对象的内存地址。 栈内存的增长方向通常是由高地址向低地址
  • 值类型的变量
      1. 以byte / sbtye / short / ushort为例
      1. 值类型没有实例,所谓的“实例”与变量合而为一 也就是说,int x; 相当于 int x = new int x();
  • 引用类型的变量与实例
      1. 引用类型变量与实例的关系:引用类型变量里存储的数据是对象的内存地址
  • 变量的默认值
  • 常量(值不可改变的变量) const
  • 装箱与拆箱(Boxing & Unboxing)
    • 当把一个值类型转换为object类型时,会发生装箱操作。 (object obj ,obj 变量是在栈上的) 例如,将一个整数赋值给object变量:
      • 在这个过程中,会在堆上创建一个新的对象,并将值类型的值复制到这个对象中。同时,在栈上会存储对这个堆上对象的引用。
    • 当从object类型转换回值类型时,会发生拆箱操作。例如:
      • 在拆箱过程中,首先要检查堆上的对象是否为正确的类型,如果是,则将值从堆上的对象复制到栈上的值类型变量中。
        总的来说,装箱和拆箱操作涉及到堆和栈的交互。 装箱时,值类型的值被复制到堆上的对象中,栈上存储对堆上对象的引用; 拆箱时,从堆上的对象中复制值到栈上的值类型变量中。 这些操作会带来一些性能开销

五、方法的定义、调用和调试*

方法在创建时,被static 修饰 会变为 类的方法。 否则为 实例的方法。
实例的方法只有在创建出实例后,用实例引出。
静态的方法会一直占用内存, 实例 则可以被释放
 
调用方法的()为 方法调用操作符

构造器(一种特殊的方法)

构造器(constructor)是类型的成员之一
狭义的构造器指的是“实例构造器'(instance constructor )

声明和调用构造器

在 C# 中,当我们写好一个类之后,如果没有进行额外的操作,系统会自动提供一个默认的构造器。这个默认构造器会将类中没有赋值的参数初始化为默认值。例如:
此时若调用 Console.WriteLine(student.ID); 和 Console.WriteLine(student.Name);,那么输出就是 0 和空字符串。 我们也可以自己创建构造器。由于构造器没有输入与输出,所以可以像下面这样创建:
这样,当创建一个 Student 类的实例时,使用默认构造器初始化后的输出就是 1 和 "No Name"。 当然,我们还可以在创建实例时要求给变量初始值,这就是构造函数重载。例如:
现在,当我们运行以下代码:
输出结果为:先是 1 和 "No Name",接着是一个分隔符 "==================",然后是 2 和 "Hello"。通过构造函数重载,我们可以根据不同的需求灵活地初始化类的实例。

构造器的内存原理

在 C# 中,对于类Student含有int IDstring Name两个成员变量。
当类中没有显式编写构造器时,即使用默认构造器。 此时创建实例Student stu = new Student();,在内存中的操作如下:首先在栈区为变量stu分配内存,该内存用于保存堆区中实例的地址。接着在堆区为int类型的IDstring类型的Name各分配四个字节的内存。其中,int类型的四个字节会以小端的补码形式存入,而string类型在默认情况下会被初始化为指向空字符。
当类中自己创建了构造器,并且在构造器中将ID赋值为 1,Name赋值为 "Hello"。当创建实例Student stu = new Student();时,内存操作过程与默认构造器类似,首先在栈区为stu变量分配内存以保存堆区实例地址。然后在堆区为intstring各分配四个字节内存。对于int类型依旧以小端的方式存入数字 1。对于string类型,系统会在堆区另外开辟足够大的内存空间来存储 "Hello" 字符串,然后Name会指向堆区这个存储 "Hello" 的地址,以小端的补码形式存储在内存中。
总之,构造器的不同 会影响堆区中对象的初始化方式,进而影响内存的分配和数据的存储。

方法的重载(Overload )

声明带有重载的方法
方法签名由方法的名称、类型形参的个数以及它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成。需注意,方法签名不包含返回类型。 同样,实例构造器签名由它的每一个形参(按从左到右的顺序)的类型和种类组成。
重载决策是在给定了参数列表和一组候选函数成员的情况下,选择一个最佳函数成员来实施调用。例如,在 C# 中常见的 Console.WriteLine 方法就有多种重载形式,可以根据不同的参数类型输出不同的内容。
下面以一个简单的计算器类为例:
在这个例子中,Calculator类中的Add方法有多个重载版本。当调用这个方法时,编译器会根据传入的参数来确定具体调用哪个重载的方法。
对于方法的重载,作为初学者要记住,方法的名字可以相同,但它的参数列表不能完全一样,即参数的个数、类型或种类不同,从而形成不同的方法签名。这样,在调用方法时,编译器可以根据传入的参数来确定具体调用哪个重载的方法。

如何对方法进行debug

Call Stack 调用堆栈 :可以看注意看栈的深度(函数之间的调用调用调用)
还有看变量,这个很熟悉了不写了

方法的调用与栈*

栈内存的增长方向是由高字节位向低字节位发展,当不断进行方法调用而栈内存使用达到其最大限度时(层级过深),就会出现 “Stack overflow(栈溢出)” 的情况。
对栈帧(stack frame)进行分析可以更好地理解方法调用在栈中的布局。首先,程序从main方法开始执行,main方法会占用一部分栈空间。当main方法调用其他方法时,main方法成为主调者(caller),被调用的方法成为被调者(callee)。
在调用方法时,传入的参数如果是变量,这些变量会被压入栈中。在 C# 语言中,这些变量由主调者(这里即main方法)管理,并且是从左往右依次压入栈的。所以,传入的变量都处于主调者的栈帧中。
接着进入被调用的方法,被调用者的局部变量由被调用方法管理。如果在这个方法中又调用了其他方法,同样遵循上述规则。
当方法执行完毕准备退出时,会首先将返回值存入 CPU 寄存器(如果寄存器空间不够,可能会采用在栈中开辟空间。CPU寄存器根据型号不同个数不同。一般就几个)。然后,销毁被调用方法中的局部变量。接着,销毁上一个调用者(主调者)传过来的参数。最后,将返回值赋值给上一个调用者的某个局部变量,以便主调者继续执行后续代码。
总的来说,栈按照压入的顺序进行操作,后进先出。每个方法的调用都会在栈上创建一个新的栈帧,包含方法的参数和局部变量等信息。当方法执行完毕,对应的栈帧被销毁,释放内存空间。这种栈的管理方式确保了方法调用的正确执行和内存的有效管理。

六、操作符详解

  • 操作符也叫运算符(Operator)
  • 操作符是用来操作数据的,被操作符操作的数据称为操作数(Operand)

操作符概览

notion image
  • 从上往下优先级依次降低
  • 只有赋值操作符是从右往左运算

操作符的本质

  • 操作符的本质是函数(即算法)的“简记法”
    • 假如没有发明”+”、只有Add函数,算式3+4+5将可以写成Add(Add(3,4),5)
    • 假如没有发明“x” 、只有Mul函数,那么算式3+4x5将只能写成Adǎ(3,Mul(4,5))
  • 操作符不能脱离与它关联的数据类型
    • 可以说操作符就是与固定数据类型相关联的一套基本算法的简记法
    • 比如 int a ,b a/b 这里的/是与证书相关联的。 同理double也是一样
举个栗子,我们来自己创建一个关联我们的函数的 数据类型 通过这个例子,我们可以清晰地知道。 C# 的操作符本质上 就是 方法 的简记法

一个栗子

操作符的优先级

  • 操作符的优先级
    • 可以使用圆括号提高被括起来表达式的优先级
    • 圆括号可以嵌套
    • 不像数学里有方括号和花括号,在C#语言里““[ ]"与”{ }“有专门的用途
  • 同优先级操作符的运算顺序
    • 除了带有赋值功能的操作符,同优先级操作符都是由左向右进行运算
    • 带有赋值功能的操作符的运算顺序是由右向左
    • 与数学运算不同,计算机语言的同优先级运算没有“结合率”
      • 3+4+5只能理解为Add(Add(3,4),5)不能理解为Add(3,Add(4,5))

各类操作符的示例

  1. 成员访问运算符“.”:用于访问类中的成员或者访问位于某个名空间中的类
  1. typeof操作符:可以用于获取一个类型的完整信息,包括完整的类型名称,该类型包含在哪个命名空间中,该类型包含哪些成员等等
  1. default:该运算符用于获取一个类型的默认值,比如:
  1. var操作符:var可以声明隐式变量,他没有数据类型。它会根据赋值到的变量实例自动推断自己的类型。
  1. new操作符:new有两种用法
    1. 作为操作符,用于创建内存实例,并且立刻调用这个实例的实例构造器。并且返回实例的地址。
        • 并且可以在实例后加入花括号,为实例的属性设置它的值。比如Form myFrom = new Form() {text = “Hello”};
        • 当然有时候也不用new ,比如string , 这是一个类类型,是为了规范,所以让我们省去了new 操作符,来创建 string实例
        • 以上都是非匿名类型的。 new 操作符的使用也不一定非得跟一个数据类型, new后也可以不跟东西。比如 var person = new {Name = “Haha”, Num = 34}; (编译器会自动推断变量的类型) 这个 就是一个隐式变量。 他的类型会根据约定好的方式进行命名<>f__AnonymousType0\2(f__AnonymousTyp 为 约定前缀,0为,程序中第1个,\2指这是一个泛型类,构成这个类型需要两个类型。) 所以我们可以直接使用这个 person 实例
        • #使用new操作符要小心,因为在类的方法中创建实例后,这个类就会和这个实例对应的对象所处的类紧密耦合,当后者 出现问题,前者也不能正常运行。产生了依赖。(为了解决这个,有个技术叫设计模式,可以有依赖注入,可以把紧耦合变为相对松一点的。)
    2. 作为修饰符,作为关键字 在类成员前面 表示子类的方法 隐藏 父类方法
        • 派生(继承):用法是子类方法名:父类方法名 子类 会继承 父类的所有方法。 子类可以在类中创建与父类 相同名称的方法,并在前缀加入new 修饰符。来覆盖(或隐藏)父类的方法。然后在子类中调用方法就是子类中创建的方法。父类调用则还是父类的方法。
  1. checked、unchecked 用来判断一个值是否在内存中溢出。
      • 对一个已经赋值为最大值的 x = checked(x+1) 会弹出警告,程序无法运行
      • 可以使用try catch 来抓住异常并处理异常
      • C#中默认为unchecked
      • 可以使用checked { } 语句块中所有溢出都会被检测到
  1. sizeof 操作符使用 时,如果要求结构体,需要把它放到不安全的上下文中才可以(unsafe{ })
  1. ->操作符
      • C#中规定 指针操作、取地址操作、以及用指针访问成员额操作 只能操作结构体类型,不能操作引用类型。
      • 同理,也要用unsafe 来放到不安全的上下文中
 

七、表达式和语句

表达式

表达式的定义:
  • 表达式是任何一门编程语言的基本组件最重要的组成部分之一
  • 表达式是专门用来求值的一种语法实体
C#对表达式的定义
  • 表达式是由一个或多个操作数和零个或多个操作符组成的序列,这些操作符可以求值为单个值、对象、方法或名称空间。
  • 得到值:x++,x = 100, --x等。他们的结果是一个值
  • 得到对象:new Form() 其中new为操作符, Form为操作数、()为调用它的构造函数。他们的结果是一个对象
  • 得到方法:Action myAction = new Action(Console.WriteLine);
    • 其中.为成员访问表达式,操作数有两个,在这个函数中左边是类型,右边是类型的成员
    • 这个表达式得到的结果,是一个方法。 方法也是一个语法实体。
  • 得到名称空间:System.Windows.Forms.Form myForm = new Form()
    • 其中,.是名称空间访问表达式,在这里。左边为主名称空间,.右边是子名称空间。这个表达式经过求职之后得到的值就是一个名称空间。
  • 表达式可以由文字值、方法调用、操作符及其操作数或简单名称组成。
  • 简单名称可以是变量、类型成员、方法参数、名称空间或类型的名称
  • 算法逻辑的基本(最小)的组成单元,表达一定的算法意图
  • 因为操作符有优先级,所以表达式也就有了优先级。
notion image

语句的定义

在计算机编程语言中,语句是命令式编程语言中最小的独立元素。
  • 命令式语言:大部分是高级语言,比如C# C++ JAVA等
语句,是一种语法实体。他的功能是表达一些将被执行的动作。
语句的内部组件就是表达式,每一个表达式都对应一个这个语句索要执行的一个动作。
一个高级语言所写成的程序,它实际上是由一系列语句所构成的
低级语言例如汇编语言和机器语言,他们只有指令,对应着CPU可以直接置信的动作
 
例如我们可以在C语言 或C#语言编写后,勾选输出汇编文件或使用 对应的工具来查看C或C#语言编译出的的汇编代码
 
程序之中,语句执行的顺序称为控制流(执行流)
C#语言的语句除了能够让程序员“顺序地”(sequentially)表达算法思想,还能通过条件判断、跳转和循环等方法控制程序逻辑的走向。
简言之就是: 陈述算法思想,控制逻辑走向,完成有意义的动作(action)
C#语言的语句由分号( ; )结尾,但由分号结尾的不一定都是语句
  • 例如引入名称空间。using 指令。
  • 例如public string Name; 字段的声明
语句一定是出现在方法体里

语句详解

C#语言的语句三大类

  • 标签语句
    • 前面带标签的语句,用得不多
  • 声明语句
    • 用来声明变量,常量、
  • 嵌入式语句
  • 被嵌入在其他语句当中的语句,就称为嵌入式语句
    • notion image

C#语言语句详解

  1. 声明语句
      • 比如 int a ; int a = 100;(注意这俩,一个是声明,后续可赋值。一个是声明后追加初始化器)
  1. 表达式语句
      • 语句表达式+ ; = 表达式语句。 (语句表达式是可以形成语句的表达式。不是所有表达式都可以作为语句来使用的)
        • 方法调用表达式(); 比如打印“Hello World” 就是选择调用只接收一个字符串类型的重载。
        • 对象创建表达式 new :new操作符会创建一个对象,并调用对象的构造器。
        • 赋值语句
        • 后置(前置)自增(自减)表达式
        • await- expression
          • 异步编程才用到。
  1. C#语言对语句的定义比C语言严格得多 一些在C#中不能当做语句的表达式
      • 单独一行 x+y; 、 x; y;
      • 而C语言可以。
 

基于C#的串口助手

Demo1 实现基础的收发框架

实现的功能

  1. 建立WinFrom项目
  1. 熟悉常用控件

用到的控件

  • label
    • 标签
  • ComboBox
    • 下拉菜单
  • TextBox
    • 文本框
  • RichTextBox
    • 复文本框
    • 复文本框与文本框、标签的区别是,可以设置更多东西。有文本编辑等能力。
  • Button
    • 按钮

实现方法和思路

各个控件设置好位置和名称后,可以看到,在这个程序里,软件已经自动实例化了各个控件的对象,并且配置好了基础的属性,并在最后声明了这些实例化的对象。
notion image
notion image
下面再摸索一下,在窗口加载时初始化一些对象中的属性,需要在这个窗口控件的 这个事件下双击配置
notion image
双击后,软件会自动生成一私有的方法, 可以在方法内对 声明的 实例化的对象进行属性的配置。如图
notion image
下面是发送按钮的配置,同上,也是在事件中,找到点击事件(也可以直接双击button控件)
notion image
下面在私有的方法中配置如下内容,实现发送的基本逻辑

Demo2 创建界面

实现的功能

  1. 完善界面规划
      • 配置区
      • 发送区
      • 接收区
  1. 添加布局所有控件

用到的控件

  • label
    • 标签
  • ComboBox
    • 下拉菜单
  • TextBox
    • 文本框
  • RichTextBox
    • 复文本框
  • Button
    • 按钮
  • GropBox
    • 分组控件
  • CheckBox
    • 选择框控件
  • StatusStrip
    • 状态栏控件

实现方法和思路

  1. 使用分组控件,分出三个区域,分别用作串口配置、接收配置和发送配置
  1. 使用选择框控件,用作功能开关
  1. 使用状态栏控件, 创建StatusLabel标签,用于状态指示
    1. 如果和下图一样,“初始化正常”字样的占长 修改的话, 需要修改下 状态图标的Spring(是否填满剩余空间)和AutoSize(自动调整项的大小) 设置为False
  1. 最终效果如图
notion image

Demo3 串口了解

实现的功能

  1. 修改各个控件的name,使其更加规范
  1. 了解串口类
      • string PortName(端口号)
      • int DataBits(数据位)
      • StopBits StopBits(停止位)
      • Parity Parity(校验位)
      • int BaudRate(波特率)
      • bool IsOpen(状态)DtrEnable / RtsEnable
      • 获取端口号:RegistryKey keyCom = Registry.LocalMachine.OpenSubKey(@"Hardware\DeviceMap\SerialComm");
      • 初始化配置参数,主要包括:端口号、波特率、数据位、停止位、校验位、编码格式 serialPort1.Encoding = Encoding.GetEncoding("GB2312");

用到的控件

  • label
    • 标签
  • ComboBox
    • 下拉菜单
  • TextBox
    • 文本框
  • RichTextBox
    • 复文本框
  • Button
    • 按钮
  • GropBox
    • 分组控件
  • CheckBox
    • 选择框控件
  • StatusStrip
    • 状态栏控件
  • SerialPort
    • 串口控件

实现方法和思路

这个Demo只做了解SerialPort类。所以只写了,点击打开串口时,的逻辑。
在使用串口等 需要操作IO的类时,为了避免发生的错误,一般会用
来吧可能出现的错误先 抛出来。
最终代码

Demo4 串口打开、关闭

实现的功能

  1. 实现串口的初始化(port,波特率、奇偶校验、停止位)
  1. 串口开关

用到的控件(不变)

  • label
    • 标签
  • ComboBox
    • 下拉菜单
  • TextBox
    • 文本框
  • RichTextBox
    • 复文本框
  • Button
    • 按钮
  • GropBox
    • 分组控件
  • CheckBox
    • 选择框控件
  • StatusStrip
    • 状态栏控件
  • SerialPort
    • 串口控件

实现方法和思路

在上个Demo的基础上修改,添加了一个标志位,用于 开,关串口
最终代码

Demo5 获取端口及串口简单收发

实现的功能

  1. 从本地设备列表中获取串口
  1. 串口数据的发送
  1. 串口数据的接收

用到的控件(不变)

  • label
    • 标签
  • ComboBox
    • 下拉菜单
  • TextBox
    • 文本框
  • RichTextBox
    • 复文本框
  • Button
    • 按钮
  • GropBox
    • 分组控件
  • CheckBox
    • 选择框控件
  • StatusStrip
    • 状态栏控件
  • SerialPort
    • 串口控件

实现方法和思路

  1. 获取系统端口
    1. 获取系统端口需要使用到注册表的一个类,从类中读取设备号
      RegistryKey keyCom = Registry.LocalMachine.OpenSubKey(@"Hardware\DeviceMap\SerialComm");
      实际上就对应了我们 计算机设备ide注册表的这个目录
      notion image
  1. 串口数据发送方法
      • public void Write(byte[] buffer, int offset, int count);
        • 写入字节数组
      • public void Write(string text);
        • 写入字符串
      • public void Write(char[] buffer, int offset, int count);
        • 写入字符数组
      • public void WriteLine(string text);
        • 写入一行字符串(在字符串末尾添加换行符)
  1. 串口数据接收方法
      • public int Read(byte[] buffer, int offset, int count);
        • 从字节数组中读取数据
      • public int Read(char[] buffer, int offset, int count);
        • 从字符数组中读取数据
      • public int ReadByte();
        • 读取一个字节的数据
      • public int ReadChar();
        • 读取一个字符的数据
      • public string ReadExisting();
        • 读取所有可用的数据,并以字符串形式返回
      • public string ReadLine();
        • 读取一行数据,并以字符串形式返回
      • public string ReadTo(string value);
        • 读取数据直到遇到指定的字符串,并以字符串形式返回。
      • public event SerialDataReceivedEventHandler DataReceived;
        • 接收到数据时触发,使用SerialDataReceivedEventHandler类型的委托来处理事件。
最终代码
  1. 获取系统端口:
    1. 点击发送按钮:
      1. 点击接收按钮:
        1. 其他

          Demo6 串口收发优化

          实现的功能

          1. 异步线程:更新UI
          1. 接收字符处理
          1. 发送字符处理

          用到的控件(添加了timer 定时器)

          • label
            • 标签
          • ComboBox
            • 下拉菜单
          • TextBox
            • 文本框
          • RichTextBox
            • 复文本框
          • Button
            • 按钮
          • GropBox
            • 分组控件
          • CheckBox
            • 选择框控件
          • StatusStrip
            • 状态栏控件
          • SerialPort
            • 串口控件
          • Timer
            • 定时器

          实现方法和思路

          1. 异步线程
              • 为什么要使用异步线程?
                • 因为串口的data receive 接收,是在辅助线程执行,数据需要更新到ui的主线程时,这个操作就会跨线程,跨线程是不安全的。
                  这时候就需要执行一个异步线程来执行数据䣌更新
          1. 数据的接收处理
              • 将数据接收,并缓存到数据缓冲区(List<byte> 优于 byte[] )
              • 字符的编码格式:GB2312、UTF8 等等。。
          1. 数据的发送处理
              • 发送普通字符串
              • 发送16进制数据
          代码部分——————————————
          • 异步线程:更新UI代码
            • 这里是为了防止一个线程来操作另一个线程,这样会导致问题出现。
          • 接收数据处理
              1. 数据处理并显示在发送文本框
                1. 其中包含了数据的处理和 累加接收长度
              1. 16进制勾选逻辑
                1. 实现在勾选和取消勾选16进制时,使接收文本框内的数据变为相应的进制。
              1. 清空接收数据
                1. 自动清空
                  1. 这里使用了一个新的控件:Timer,(设置为100ms一次的定时),当自动清空被勾选时,会打开定时器,定时器则会1s检测10次 接收框的数据长度,如果大于4096(默认)那么就清空。(后续会改为可设置)
                1. 暂停接收
              • 发送数据处理
                  1. 数据处理并显示在接收文本框
                    1. 这里使用到了一个新的事件:发送文本框的Leave 离开焦点事件,当焦点离开时,执行下面的方法。目的时检测当前数据是否为16进制数据,如果不是,则发送相应警告。
                      根据选择, 处理或者不处理数据,让后放到发送缓冲区中
                      其中包含了数据的处理和 累加发送长度
                      注意如果使用文本发生变化事件,去处理16进制的转换,会发生错误 问题就是0x00 转换
                      sendBuffer.AddRange(Transform.ToBytes(send_rtb.Text.Replace(" ", "")));
                      }
                      else //不是16进制数据
                      {
                      MessageBox.Show("请输入正确的十六进制数据!!", "发送框警告");
                      send_rtb.Select();
                      }
                      }
                      else //未选中16进制。
                      {
                      sendBuffer.Clear();
                      //把当前数据,存放到发送缓冲区。
                      sendBuffer.AddRange(Encoding.GetEncoding("gb2312").GetBytes(send_rtb.Text));
                      }
              }
              //发送 16进制切换 框 点击
              private void sendhex_chb_CheckedChanged(object sender, EventArgs e)
              {
              //判断发送框文本你是否为空
              if (send_rtb.Text == "") return;
              //把发送缓冲区的所有 变为 16进制, 用空格隔开
              if (sendhex_chb.Checked == true)
              {
              send_rtb.Text = Transform.ToHexString(sendBuffer.ToArray(), " ");
              }
              else
              {
              send_rtb.Text = Encoding.GetEncoding("gb2312").GetString(sendBuffer.ToArray()).Replace("0","\0");
              }
              }
              //发送框清除按钮点击事件
              private void sendclear_btn_Click(object sender, EventArgs e)
              {
              sendBuffer.Clear(); //清除发送框缓冲区
              send_rtb.Text = ""; //清除发送框
              sendCount = 0; //清除发送计数
              sencount_tssl.Text = "0"; //清除发送框计数
              }
              using Microsoft.Win32;
              using System;
              using System.Collections.Generic;
              using System.ComponentModel;
              using System.Drawing;
              using System.IO.Ports;
              using System.Text;
              using System.Windows.Forms;
              namespace 串口助手
              {
              public partial class Form1 : Form
              {
              //串口标志位
              private bool isOpen = false;
              //暂停标志位
              private bool isRxShow = true;
              //接收缓冲区
              private List<byte> reciveBuffer = new List<byte>();
              //发送缓冲区
              private List<byte> sendBuffer = new List<byte>();
              //接收计数
              private int reciveCount = 0;
              //发送计数
              private int sendCount = 0;
              //实例化SerialPort窗口事件
              public Form1()
              {
              InitializeComponent();
              Control.CheckForIllegalCrossThreadCalls = false;
              }
              //加载窗口控件事件
              private void Form1_Load(object sender, EventArgs e)
              {
              serialLoad();
              }
              //自定义私有方法,初始化串口配置等
              private void serialLoad()
              {
              EncodingInfo[] encodingInfos = Encoding.GetEncodings();
              RegistryKey keyCom = Registry.LocalMachine.OpenSubKey(@"HardwareDeviceMapSerialComm");
              string[] sSubKeys = keyCom.GetValueNames();
              port_cbb.Items.Clear();
              foreach (var sValue in sSubKeys)
              {
              string portName = (string)keyCom.GetValue(sValue);
              port_cbb.Items.Add(portName);
              }
              this.port_cbb.SelectedIndex = 0;
              this.baud_cbb.SelectedIndex = 1;
              this.check_cbb.SelectedIndex = 0;
              this.databit_cbb.SelectedIndex = 3;
              this.stopbit_cbb.SelectedIndex = 0;
              }
              //发送
              private void sendData()
              {
              //写串口,发送缓冲区中的数据
              serialPort1.Write(sendBuffer.ToArray(),0,sendBuffer.Count);
              //累加发送数据长度
              sendCount += sendBuffer.Count;
              //更新数据长度到状态
              sencount_tssl.Text = sendCount.ToString();
              }
              private void send_btn_Click(object sender, EventArgs e)
              {
              // 如果发送的数据不为空,则发送
              if (this.send_rtb.Text != "" && serialPort1.IsOpen)
              {
              Console.WriteLine(Transform.ToHexString(sendBuffer.ToArray()));//调试用
              sendData(); //调用自己的发送方法
              }
              else
              {
              MessageBox.Show("请先输入发送数据!");
              }
              }
              //点击打开串口按钮
              private void open_btn_Click(object sender, EventArgs e)
              {
              try
              {
              if (serialPort1.IsOpen == false)
              {
              //设置基础配置
              serialPort1.PortName = port_cbb.Text;
              serialPort1.BaudRate = Convert.ToInt32(baud_cbb.Text);
              serialPort1.DataBits = Convert.ToInt32(databit_cbb.Text);
              switch (check_cbb.SelectedIndex)
              {
              // none odd even
              case 0:
              serialPort1.Parity = Parity.None;
              break;
              case 1:
              serialPort1.Parity = Parity.Odd;
              break;
              case 2:
              serialPort1.Parity = Parity.Even;
              break;
              default:
              serialPort1.Parity = Parity.None;
              break;
              }
              switch (stopbit_cbb.SelectedIndex)
              {
              // 1 1.5 2
              case 0:
              serialPort1.StopBits = StopBits.One;
              break;
              case 1:
              serialPort1.StopBits = StopBits.OnePointFive;
              break;
              case 2:
              serialPort1.StopBits = StopBits.Two;
              break;
              default:
              serialPort1.StopBits = StopBits.One;
              break;
              }
              isOpen = true;
              open_btn.Text = "关闭串口";
              }
              else
              {
              serialPort1.Close();
              isOpen = false;
              open_btn.Text = "打开串口";
              }
              }
              catch (Exception ex)
              {
              MessageBox.Show(ex.ToString()+serialPort1.PortName.ToString());
              }
              }
              //串口接收到数据事件
              private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
              {
              //如果暂停了,那么就不显示在接收框,直接return
              if (isRxShow == false) return;
              // 1,需要读取有效的数据 BytesToRead
              //创建一个 数组,大小是当前串口缓存中的字节数量
              byte[] dataTemp = new byte[serialPort1.BytesToRead];
              //读取数据。
              serialPort1.Read(dataTemp,0,dataTemp.Length);
              //添加一组(集合)数据到接收框的末尾
              reciveBuffer.AddRange(dataTemp);
              //计数下接收到的数据
              reciveCount += dataTemp.Length;
              // 使用 Invoke 方法确保在 UI 线程上执行以下操作,防止异步线程
              this.Invoke(new EventHandler(delegate //委托给 UI 线程
              {
              // 显示接收数据的长度
              recivecount_tssl.Text = reciveCount.ToString();
              //如果没有勾选16进制
              if (!recivehex_chb.Checked)
              {
              // 通过指定的编码格式来获取字符串
              string str = Encoding.GetEncoding("gb2312").GetString(dataTemp);
              // 3,0x00 -> 0 结束 不会显示,所以转义成0 显示出来
              str=str.Replace("0", "\0");
              //向接收文本框追加处理后的数据
              recive_rtb.AppendText(str);
              //下面等同于上面三句话
              //recive_rtb.AppendText(Encoding.GetEncoding("gb2312").GetString(dataTemp).Replace("0", "\0"));
              }
              else
              {
              // 十六进制是选中的状态下
              //向接收文本框追加 将byte转化为16进制的 数据,然后追加一个空格
              recive_rtb.AppendText(Transform.ToHexString(dataTemp, " "));
              }
              }));
              }
              //暂停按键事件
              private void stop_btn_Click(object sender, EventArgs e)
              {
              if (isRxShow == true)
              {
              isRxShow = false;
              stop_btn.Text = "取消暂停";
              }
              else
              {
              isRxShow = true;
              stop_btn.Text = "暂停";
              }
              }
              //接收 16进制切换 框 点击
              private void recivehex_chb_CheckedChanged(object sender, EventArgs e)
              {
              //如果接收缓冲区 为空,直接退出
              if (recive_rtb.Text == "") return;
              //如果16进制为勾选状态
              if (recivehex_chb.Checked)
              {
              //把接收缓冲区的所有 变为 16进制, 用空格隔开
              recive_rtb.Text = Transform.ToHexString(reciveBuffer.ToArray()," ");
              }
              else
              {
              //将接收到的字节数据根据 GB2312 编码转换为字符串,替换不可见的空字符 0 为可见的 \0
              recive_rtb.Text = Encoding.GetEncoding("gb2312").GetString(reciveBuffer.ToArray()).Replace("0","\0");
              }
              }
              //清空接收的数据
              private void clear_btn_Click(object sender, EventArgs e)
              {
              reciveBuffer.Clear();
              recivecount_tssl.Text = "";
              recive_rtb.Text = "";
              }
              //自动清空点击事件
              private void autoclear_chb_CheckedChanged(object sender, EventArgs e)
              {
              //如果勾选了自动清空,那么启动定时器
              if (autoclear_chb.Checked)
              {
              timer1.Start();
              }
              else
              {
              timer1.Stop();
              }
              }
              //Timer定时事件到(100ms检测一次)
              private void timer1_Tick(object sender, EventArgs e)
              {
              //如果接收框内容大于4096,那么自动清除
              if (recive_rtb.Text.Length > 4096)
              {
              reciveBuffer.Clear(); //清除接收缓冲区
              recive_rtb.Text = ""; //清除接收框
              recivecount_tssl.Text = ""; //清除计数
              }
              }
              //当焦点移开发送框时触发
              private void send_rtb_Leave(object sender, EventArgs e)
              {
              //判断是否要发送16进制数据(是否为选中状态)
              if (sendhex_chb.CheckState == CheckState.Checked)
              {
              //选中了16进制发送
              //把所有空格删除后,判断这是不是一个16进制字符串
              if (DataEncoding.IsHexString(send_rtb.Text.Replace(" ", "")))
              {
              sendBuffer.Clear();
              //把转换好的数据,存放到发送缓冲区
              sendBuffer.AddRange(Transform.ToBytes(send_rtb.Text.Replace(" ", "")));
              }
              else //不是16进制数据
              {
              MessageBox.Show("请输入正确的十六进制数据!!", "发送框警告");
              send_rtb.Select();
              }
              }
              else //未选中16进制。
              {
              sendBuffer.Clear();
              //把当前数据,存放到发送缓冲区。
              sendBuffer.AddRange(Encoding.GetEncoding("gb2312").GetBytes(send_rtb.Text));
              }
              }
              //注意如果使用文本发生变化事件,去处理16进制的转换,会发生错误
              private void send_rtb_TextChanged(object sender, EventArgs e)
              {
              // 十六进制切换 会出现问题 这问题就是0x00 转换
              }
              //发送 16进制切换 框 点击
              private void sendhex_chb_CheckedChanged(object sender, EventArgs e)
              {
              //判断发送框文本你是否为空
              if (send_rtb.Text == "") return;
              //把发送缓冲区的所有 变为 16进制, 用空格隔开
              if (sendhex_chb.Checked == true)
              {
              send_rtb.Text = Transform.ToHexString(sendBuffer.ToArray(), " ");
              }
              else
              {
              send_rtb.Text = Encoding.GetEncoding("gb2312").GetString(sendBuffer.ToArray()).Replace("0","\0");
              }
              }
              //发送框清除按钮点击事件
              private void sendclear_btn_Click(object sender, EventArgs e)
              {
              sendBuffer.Clear(); //清除发送框缓冲区
              send_rtb.Text = ""; //清除发送框
              sendCount = 0; //清除发送计数
              sencount_tssl.Text = "0"; //清除发送框计数
              }
              }
              }
              private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
              {
              if (isRxShow == false) return;
              // 1,需要读取有效的数据 BytesToRead
              byte[] dataTemp = new byte[serialPort1.BytesToRead];
              serialPort1.Read(dataTemp,0,dataTemp.Length);
              reciveBuffer.AddRange(dataTemp);
              reciveCount += dataTemp.Length;
              this.Invoke(new EventHandler(delegate
              {
              // 显示接收数据的长度
              recivecount_tssl.Text = reciveCount.ToString();
              //如果数据帧格式没有被选中,那么就按照原先逻辑
              if (startData_chb.Checked == false)
              {
              if (!recivehex_chb.Checked)
              {
              // 2, 编码格式的选择
              string str = Encoding.GetEncoding("gb2312").GetString(dataTemp);
              // 3,0x00 -> 0 结束 不会显示
              str = str.Replace("0", "\0");
              recive_rtb.AppendText(str);
              // recive_rtb.AppendText(Encoding.GetEncoding("gb2312").GetString(dataTemp).Replace("0", "\0"));
              }
              else
              {
              // 十六进制是选中的状态下
              recive_rtb.AppendText(Transform.ToHexString(dataTemp, " "));
              }
              }
              else//如果选中了,那么久按照数据帧逻辑
              {
              // 解析数据 queue
              foreach (byte item in dataTemp)
              {
              // 入列
              bufferQueue.Enqueue(item);
              }
              // 解析获取帧头
              if (isHeadRecive == false)
              {
              foreach (byte item in bufferQueue.ToArray())
              {
              //查找帧头
              if (item != 0x7f)
              {
              // 出列
              bufferQueue.Dequeue();
              Console.WriteLine("not 0x7f, Dequeue !!");
              }
              else
              {
              // get 0x7f from bufferQueue
              isHeadRecive = true;
              Console.WriteLine("0x7f is recived !!");
              break;
              }
              }
              }
              //接收数据
              if (isHeadRecive == true)
              {
              // 判断有数据帧长度
              if (bufferQueue.Count >= 2)
              {
              //调试数据
              Console.WriteLine(DateTime.Now.ToLongTimeString());
              Console.WriteLine($"show the data in bufferQueue{Transform.ToHexString(bufferQueue.ToArray())}");
              Console.WriteLine($"frame lenth ={String.Format("{0:X2}", bufferQueue.ToArray()[1])}");
              frameLenth = bufferQueue.ToArray()[1];
              // 一帧完整的数据长度判断,不代表数据是正确的
              if(bufferQueue.Count>=1+1+frameLenth+2) //帧头、数据、数据、CRC校验
              {
              //把有效的帧长度拷贝出去
              byte[] frameBuffer = new byte[1 + 1 + frameLenth + 2];
              Array.Copy(bufferQueue.ToArray(), 0, frameBuffer, 0, frameBuffer.Length);
              if(crc_chech(frameBuffer)) //CRC校验有效则进入
              {
              Console.WriteLine("frame is check ok,pick it");
              data_txb.Text = Transform.ToHexString(frameBuffer);
              data1_txb.Text = String.Format("{0:X2}", frameBuffer[2]);
              data2_txb.Text = String.Format("{0:X2}", frameBuffer[3]);
              data3_txb.Text = String.Format("{0:X2}", frameBuffer[4]);
              data4_txb.Text = String.Format("{0:X2}", frameBuffer[5]);
              }
              else
              {
              // 无效数据
              Console.WriteLine("bad frame, drop it");
              }
              for(int i=0; i<1 + 1+frameLenth+2;i++) //抛弃(出列),
              {
              bufferQueue.Dequeue();
              }
              //处理下一帧数据
              isHeadRecive = false;
              }
              }
              //接续接收数据
              }
              }
              }));
              }
              //CRC校验,自己定义的。
              private bool crc_chech(byte[] frameBuffer)
              {
              /*大端模式: 是指数据的高字节保存在内存的低地址中,
              • 而数据的低字节保存在内存的高地址中,这样的存储
              • 模式有点儿类似于把数据当作字符串顺序处理:地址
              • 由小向大增加,而数据从高位往低位放;这和我们的
              • 阅读习惯一致。
                • 小端模式: 是指数据的高字节保存在内存的高地址中,
                • 而数据的低字节保存在内存的低地址中,这种存储模
                • 式将地址的高低和数据位权有效地结合起来,高地址
                • 部分权值高,低地址部分权值低。
                */
                bool ret = false;
                byte[] temp = new byte[frameBuffer.Length - 2];
                Array.Copy(frameBuffer, 0, temp, 0, temp.Length);
                byte[] crcdata = DataCheck.DataCrc16_Ccitt(temp, DataCheck.BigOrLittle.BigEndian); //使用大端 CRC校验
                if (crcdata[0] == frameBuffer[frameBuffer.Length - 2] &&
                crcdata[1]== frameBuffer[frameBuffer.Length-1]) //如果校验值相等
                {
                // check ok
                ret = true;
                }
                return ret;
                }
                上一篇
                沁恒CH32环境搭建与IDE使用
                下一篇
                LoRa学习

                评论
                Loading...