阅读声明:本文纯记录自己学习时的笔记,因为自己先前有过C语言基础,因此部分细节笔记不包含其中,还有部分笔记及代码来自其他论坛和博客

该笔记会随着自己的学习不断加深和完善
C# 教程 | 菜鸟教程https://www.runoob.com/csharp/csharp-tutorial.html希望对同为学习C#语言的你有所启发。

C#程序结构

一个C#程序主要包括以下部分

命名空间声明

class

class属性

一个Main函数

语句或者表达式

注释

using System;   //命名空间Sytem引用
namespace HelloWorldApplication        //命名空间声明
{
   internal class HelloWorld            //class属性 class方法 class类名
   {
      static void Main(string[] args)        //main主程序入口函数
      {
         Console.WriteLine("Hello World");
         Console.ReadKey();
        /*这是一段注释*/
      }
   }
}

C#基本语法

函数的引用方法

using System;       //引用命名空间System

namespace helloword        
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var n = 100;
            var m = 200;
            var sum = 0 ;
            sum = n = m;
           /*该行代码表示引用System命名空间Console类中的WriteLine函数*/
            System.Console.WriteLine(sum);
 
            /*如果该文件中已经使用using 引用system,则引用Sysem中的类时,则不需要加命名空间*/
            Console.WriteLine(sum);



        }
    }
}

C#是一种面向对象的编程语言。在面向对象的程序设计方法中,程序由各种相互交互的对象组成。相同种类的对象通常具有相同的类型,或者说,是在相同的class类中。

让我们来看看一个矩形类的实现,并借此讨论C#的基本语法:

例如,矩形拥有长length、width、面积三个属性。

using System;

namespace RectangleGetArea
{
    class Rectangle
    {
        double width = 0;
        double length = 0;
        public void Acceptdetails()
        {
            width = 5.0;
            length = 3.5;
        }
        public double GetArea()
        {
            return (width * length);
        }
        public void Dispay()
        { 
            Console.WriteLine("length:{0}",length); 
            Console.WriteLine("width:{0}",width);
            Console.WriteLine("Area:{0}",GetArea());
        }
    }
    class this_main
    {
        static void Main()
        {
            Rectangle r= new Rectangle();
            r.Acceptdetails();
            r.GetArea();
            r.Dispay();
        }
    }
}

逐层解析代码,领悟一下C#的基本语法结构。

首先 using System,该行用于引用System命名空间,方便接下来的Console.WriteLine方法的使用。

namespace RectangleGetArea,声明RectangleGetArea的命名空间

该命名空间有一个class类Rectangle,该class类中含两个成员变量和三个成员函数

成员变量

成员是类的属性或数据成员,用于存储数据。在上面的程序中,class类Rectangle有两个成员变量width和length。

成员函数

函数是执行一系列指定任务的语句集。类的成员函数是在类内声明的。刚刚举例的类Rectangle包含了三个成员函数void Acceptdetails()、double GetArea()、void Dispay()。

在类this_main中包含着main入口函数以及实例化的类Rectangle。

类的实例化

在任何一个命名空间中的类,需要在其他函数使用时,都需要将该类实例化,并赋予一个新的名称。该步骤叫做类的实例化。例如在上面程序中的, Rectangle r= new Rectangle();

这里要注意的是,与变量的声明不一样,类的实例化需要

                                                        类名 变量名=new 类名(参数);

而变量的声明则为

                                                        变量类型 变量名=初始值;

对于类(class):必须使用new来创建实例,否则变量为null,无法使用

new的作用就是分配内存并调用构造函数,让对象真正诞生。

在这里存在一个问题,就是static修饰后的函数不需要实例化,可以直接访问。因为经static修饰后,该类为静态的,在程序运行时已经自动分配了一个静态内存,而不经static修饰的类,还没有真正分配内存,因此不能直接访问函数。例如下面这段例程。

关于修饰符访问修饰符的概念,可以先不管这一部分,后续章节会讲到,只需要记住经static修饰后的类可以不用实例化。

class MyClass
{
    public static void StaticMethod() { }   // 静态方法
    public void InstanceMethod() { }        // 实例方法
}

// 调用静态方法:直接通过类名
MyClass.StaticMethod();

// 调用实例方法:必须先创建对象
MyClass obj = new MyClass();
obj.InstanceMethod();

C#方法(函数)

方法(函数)的定义和调用

为方便接下来更好地阅读代码,我调整了一下阅读顺序。在这小节中,先将方法的定义讲出来

方法就是函数,函数就是方法,只是有不同的说法。

一个方法是把一些相关的语句组织在一起,用来执行一个任务的语句块。每一个 C# 程序至少有一个带有 Main 方法的类。

要使用一个方法,您需要:

  • 定义方法
  • 调用方法

当定义一个方法时,从根本上说是在声明它的结构的元素。在 C# 中,定义方法的语法如下:

<Access Specifier> <Return Type> <Method Name>(Parameter List)
{
   Method Body
}

//案例
static int helloworld(int i)
{
  i=10;
  Console.WriteLine(i);
  return i;
]
  • Access Specifier:访问修饰符,这个决定了变量或方法对于另一个类的可见性。
  • Return type:返回类型,一个方法可以返回一个值。返回类型是方法返回的值的数据类型。如果方法不返回任何值,则返回类型为 void
  • Method name:方法名称,是一个唯一的标识符,且是大小写敏感的。它不能与类中声明的其他标识符相同。
  • Parameter list:参数列表,使用圆括号括起来,该参数是用来传递和接收方法的数据。参数列表是指方法的参数类型、顺序和数量。参数是可选的,也就是说,一个方法可能不包含参数。
  • Method body:方法主体,包含了完成任务所需的指令集。
//案例
using System;

namespace this_main
{
    class this_main_1
    {
        //定义static修饰符 返回值为int 方法名为helloworld 参数为int i的方法
        static int helloworld(int i)
        {
            Console.WriteLine(i);
            i = i + 1;
            return i;
        }
        static void Main(string[] args)
        {
            int b = 10, c = 0;
           //调用方法传递参数b,使用c接收返回值方法返回值
            c = this_main_1.helloworld(b);   
            Console.WriteLine(c);
        }
    }
}
/*输出结果为
10
11
*/

方法的参数传递

刚刚的案例中,已经初步领略了方法的参数传递和返回,现在重点说说参数传递部分。

按值传递参数

这与刚刚的案例一致,定义方法时,需定义值传递的类型以及变量名,调用方法时需定义一个相同值类型的变量,并将该变量在调用方法时传递。此时在外部定义的int b 我们叫做实参,传入方法内部的int i(这里传入的是b)我们称之为形参。

传递过程中形参相当于复制实参的数据并传入方法内部进行数据处理,但该过程中不会改变实参的数据。

将刚刚的例子改编一下便能验证这点。

//案例
using System;

namespace this_main
{
    class this_main_1
    {
        //定义static修饰符 返回值为int 方法名为helloworld 参数为int i的方法
        static int helloworld(int i)
        {
            i = i + 1;
            return i;
        }
        static void Main(string[] args)
        {
            int b = 10, c = 0;
            c = this_main_1.helloworld(b);      //使用返回值c接收形参
            Console.WriteLine("这是实参b:{0}",b);   //显示实参b
            Console.WriteLine("这是形参i:{0}",c);    //用c接收形参后,显示形参i
        }
    }
}
/*输出结果为
这是实参b:10
这是形参i:11
*/

按引用传递参数

引用参数是一个对变量的内存位置的引用。当按引用传递参数时,与值参数不同的是,它不会为这些参数创建一个新的存储位置。引用参数表示与提供给方法的实际参数具有相同的内存位置。

翻译成人话就是,按引用传递参数类似与C中的指针,它把保存数据的这段内存位置传递给了方法,方法找到内存位置后进行数据处理,因此实参的数据也会跟着形参改变

在 C# 中,使用 ref 关键字声明引用参数。下面的实例演示了这点

//案例
using System;

namespace this_main
{
    class This_main_1
    {
        //定义static修饰符 返回值为int 方法名为helloworld 参数为int i的方法
        static int Helloworld(ref int i)   //按引用传递
        {
            i ++;
            return i;
        }
        static void Main()
        {
            int b = 10, c ;
            c = This_main_1.Helloworld(ref b);      //使用返回值c接收按引用传递的b
            Console.WriteLine("这是实参b:{0}", b);   //显示实参b
            Console.WriteLine("这是形参i:{0}", c);    //用c接收形参后,显示形参i
        }
    }
}
/*输出结果为
这是实参b:11
这是形参i:11
*/

按输出传递参数

按输出传递参数与按引用参数传递有点类似,其都是将一段内存位置传递给方法后,方法内部找到该段内存位置对数据内容进行处理。

在 C# 中,使用 out 关键字声明引用参数。下面的实例演示了这点

//案例
using System;

namespace this_main
{
    class This_main_1
    {
        //定义static修饰符 返回值为int 方法名为helloworld 参数为int i的方法
        static void HelloWorld(out int i)
        {
            i = 50;                     //方法内将i赋值为50
        }
        static void Main()
        {
            int a = 0;                 //赋初始值0
            Console.WriteLine("这是未调用方法前的a:{0}",a);
            HelloWorld(out a);          //调用方法
            Console.WriteLine("这是调用方法后的a:{0}",a);
        }
    }
}
/*输出结果为
这是未调用方法前的a:0
这是调用方法后的a:50
*/

out与ref的不同

按引用传递和按输出传递效果一致,但有如下差异。

1、ref可进可出,变量具体数据内容可以传递给方法,方法内部也可以读取和修改这个值,修改后的值方法结束后传递出去。

2、out只出不进,变量在传入时可以不赋值,方法内部不能读取和修改它的原始值,但必须在返回前给它赋值,调用方法拿到的是方法赋的值。

案例1:ref可进可出

//案例
using System;

namespace this_main
{
    class Program
    {
        static void Main()
        {
            int a = 10;               // 必须初始化,因为要“进”
            Console.WriteLine($"调用前 a = {a}"); // 输出 10

            ModifyWithRef(ref a);

            Console.WriteLine($"调用后 a = {a}"); // 输出 30
        }

        static void ModifyWithRef(ref int num)
        {
            // 可以读取传入的值
            Console.WriteLine($"方法内读取到传入的值: {num}"); // 输出 10

            // 可以修改
            num = num + 20;            // 读取并修改

            // 修改后,方法结束时会带出去
        }
    }
}

输出:

text

调用前 a = 10
方法内读取到传入的值: 10
调用后 a = 30

案例二:out只出不进

//案例
using System;

namespace this_main
{
    class Program
    {
        static void Main()
        {
            int b;                      // 可以不初始化
            // Console.WriteLine(b);     // 这行会编译错误,因为 b 未赋值

            ModifyWithOut(out b);

            Console.WriteLine($"调用后 b = {b}"); // 输出 50
        }

        static void ModifyWithOut(out int num)
        {
            // 下面这行如果取消注释,会编译错误:使用了未赋值的 out 参数
            // Console.WriteLine(num);   // ❌ 错误!不能读取 out 参数的值

            num = 50;                    // 必须赋值,否则编译错误
            //num = num + 50;            // ❌ 错误! out只出不进,它不知道自己进来的数据是多少,+50后的结果不确定     
        }
    }
}

输出:

调用后 b = 50

总结表格

特性 ref out
调用前必须初始化
方法内能否读取参数 不能(编译器禁止)
方法内必须赋值 不一定(可以只读取) 必须(在返回前)
典型用途 修改已有变量 返回多个值(如 TryParse

具名参数与不具名参数

方法的参数传递时分为具名参数和不具名参数。

不具名参数案例

//案例
using System;
using System.Diagnostics.Eventing.Reader;

namespace this_main
{
    class Program
    {
        static void Main()
        {
            PrintInfo("Tim", 50);
        }
        static void PrintInfo(string Name,int age)
        {
            Console.WriteLine("Hello:{0},Your age:{1}", Name, age);
        }
    }
}
//结果输出Hello:Tim,Your age:50

具名参数案例

//案例
using System;
using System.Diagnostics.Eventing.Reader;

namespace this_main
{
    class Program
    {
        static void Main()
        {
            PrintInfo(Name:"Tim",age:50);    //具名参数
        }
        static void PrintInfo(string Name,int age)
        {
            Console.WriteLine("Hello:{0},Your age:{1}", Name, age);
        }
    }
}
//结果输出Hello:Tim,Your age:50

具名参数相比不具名参数优点

1、提高代码可读性,直接将方法定义的形参名表达,不用再跳转到方法定义时,查看该参数传递的意义。

2、使用具名参数后不再受到参数列表的限制,例如可以这样写

PrintInfo(age:50,Name: "Tim");

可选参数

若方法构造时参数具有默认值,当调用方法时没有对应参数输入,方法内部将使用默认参数

//案例
using System;
using System.Diagnostics.Eventing.Reader;

namespace this_main
{
    class Program
    {
        static void Main()
        {
            PrintInfo();                    //这里不输入参数
        }
        static void PrintInfo(string Name = "Tim", int age =50)    //参数具有默认值,将直接采用默认值
        {
            Console.WriteLine("Hello:{0},Your age:{1}", Name, age);
        }
    }
}
//结果输出Hello:Tim,Your age:50

扩展方法

扩展方法的注意事项如下

1、方法必须是公有、静态的,即被public static修饰

2、必须是形参列表中的第一个,由this 修饰

3、必须由一个静态类(一般类名为SomeTypeExtension)来统一收纳SomeType类型的扩展方法

通过一个实例来理解,该例中通过一个Math.Round(值,位数)方法来四舍五入一个小数

//案例
using System;
using System.Diagnostics.Eventing.Reader;

namespace this_main
{
    class Program
    {
        static void Main()
        {
            double x = 3.14159;
            double y=Math.Round(x,4);
            Console.WriteLine(y);   
        }
    }
}
//结果输出3.1416

在该例中,扩展方法的作用可能不太明显,扩展方法的主要作用为,为现有的方法扩展用途

//案例
using System;
using System.Diagnostics.Eventing.Reader;

namespace this_main
{
    class Program
    {
        static void Main()
        {
            double x = 3.14159;
            double y =x.DoubleRound(4);//构造了一个拓展方法,来对原来的Math.Round进行拓展
            Console.WriteLine(y);   
        }
    }
    static class DoubleExtension                       //拓展方法必须在一个静态类中

    {
        public static double DoubleRound(this double number, int i)  //构造扩展方法,该方法必须是公有的 静态的
        { 
           double result=Math.Round(number,i);             
            return result;
        }
   
    }
}
//结果输出3.1416

这个先不纠结,知道有这么一个东西就行。

C#数据类型

在C#中,变量分为以下几种类型:

值类型(Value types)

引用类型(Regerence types)

指针类型(Pointer types)

值类型(Value types)

需要注意的是,变量类型的尺寸并不固定,其尺寸根据具体运行的平台决定,可以使用sizeof(int)的方法获取int类型的存储尺寸。

在某些PLC产品中,int尺寸就为16有符号整数,与C#中的32位有区别

引用类型(Regerence types)

对象(Object)类型

对象(Object)类型 是 C# 通用类型系统(Common Type System - CTS)中所有数据类型的终极基类。Object 是 System.Object 类的别名。所以对象(Object)类型可以被分配任何其他类型(值类型、引用类型、预定义类型或用户自定义类型)的值。但是,在分配值之前,需要先进行类型转换。

当一个值类型转换为对象类型时,则被称为 装箱;另一方面,当一个对象类型转换为值类型时,则被称为 拆箱

/*使用代码进行解释装箱与拆箱*/

int i=10;
object o=i;    //装箱
i=20;
console.WriteLine("object");
/*此时i=20, object=10*/


object o=10;
int x=(int)o;   //拆箱,此时x=10
x=30;
/*此时x=30, o还是10*/


//多引用
object o1=10;
object o2=o1;
o1=20;
/*o2还是10*/

通俗来讲,传统的值类型接受和传递需要类型匹配,而object的装箱则不需要考虑值类型(int? short? float?),仅将值类型变量的数据复制出来并隔离。

由于装箱和拆箱需要耗费大量内存和性能,现在已有泛型代替,很少使用object。

字符串(String)类型

字符串(String)类型 允许您给变量分配任何字符串值。字符串(String)类型是 System.String 类的别名。它是从对象(Object)类型派生的。

string str = "123\n456";
/*输出结果为:
123
456*/
C# string 字符串的前面可以加 @(称作"逐字字符串")将转义字符(\)当作普通字符对待,比如:
string str = @"123\n456";
/*输出结果为:
123\n456*/

/*转义字符
编译器在执行string字符串时,针对某些特定字符将不会当做字符串处理,
而是当做转义字符处理,使之具有特殊功能
例如 \n 将被处理为换行
*/

指针类型(Pointer types)

指针类型变量存储另一种类型的内存地址。C# 中的指针与 C 或 C++ 中的指针有相同的功能。

声明指针类型的语法:

int* p1, p2, p3;     // 正确  
int *p1, *p2, *p3;   // 错误 

C#类型转换

在C#中,类型转换是将一个数据类型的值转换为另一个数据类型的过程。

C#类型转换分为隐式类型转换和显式类型转换(强制类型转换)。

隐式类型转换

隐式类型转换不需要编写指定的代码来执行转换,编译器会自动进行。

隐式类型转换只发生在从小范围数据到较大范围数据类型时,编译器会将其自动转换,这不会导致数据丢失。

例如:从int 到 long ,float 到double

int a = 10;

long b =a;

long c=;

c=100+a;

float j=1.32;

double i= j;

显式类型转换

显式类型转换类似与C中的强制类型转换,需要程序员在代码中明确指定。

因为从小范围到大范围数据类型转换,通常编译器自动执行,因此编写显式类型转换时,可能存在数据丢失或数据紊乱的情况。

C#中自带了很多类型转换函数,目前先介绍两种转换方式,其他的遇到了再详细查看C#官方手册或者询问AI。

第一种转换方式

                                                                       变量 =(显式变量类型)变量;

                                                                            b  = ( byte ) i ;

/*强制转换为byte类型*/
int i=10;
byte b = (byte)i;        //该过程i丢失高位24位,整数截断规则:丢失高位,保留低位

/*强制转换为整数类型*/
double doubleValue=3.1415926
int intValue=(int)doubleValue;     //该过程丢失小数部分,保留整数部分,intValue为3

/*double强制转换为float*/
double doubleValue=3.1415926465;
float floatValue=doubleValue;   //该过程保留前七位小数,向零取整规则,结果为3.1415926

第二种转换方式

第二种转换方式是一种较为安全的转换方式,在第一种转换方式中,可能存在变量为null(空)时的异常情况。该类方法都定义在System.Convert类中,使用时需要using System命名空间。

转换失败时,将会触发报错。

序号 方法 描述
1 ToBoolean 转换为布尔型
2 ToByte 转换为字节类型
3 ToChar 转换为Char类型
4 ToDateTime 转换为日期-时间结果
5 ToDecimal 转换为十进制类型
6 ToDouble 转换为双精度浮点型
7 ToInt16 转换为16位整数
8 ToInt32 转换为32位整数
9 ToInt64 转换为64位整数
10 ToSbyte 转换为有符号字节类型
11 ToSingle 转换为小浮点数类型
12 ToString 转换为字符串类型
13 ToType 转换为指定类型
14 ToUInt16 转换为无符号16位整数
15 ToUInt32 转换为无符号32位整数
16 ToUInt64 转换为无符号64位整数
using System;

namespace TypeConversionApplication
{
    class StringConversion
    {
        static void Main(string[] args)
        {
            // 定义一个整型变量
            int i = 75;
           
            // 定义一个浮点型变量
            float f = 53.005f;
            
            // 定义一个双精度浮点型变量
            double d = 2345.7652;
            
            // 定义一个布尔型变量
            bool b = true;

            // 将整型变量转换为字符串并输出
            Console.WriteLine(i.ToString());
            
            // 将浮点型变量转换为字符串并输出
            Console.WriteLine(f.ToString());
            
            // 将双精度浮点型变量转换为字符串并输出
            Console.WriteLine(d.ToString());
            
            // 将布尔型变量转换为字符串并输出
            Console.WriteLine(b.ToString());

            // 等待用户按键后关闭控制台窗口
            Console.ReadKey();
        }
    }
}

C#中变量定义

变量定义较为简单

变量类型 变量名;//无初始化

变量类型 变量名=0;//初始化为0 通常建议以下这种,赋予默认值

与C一样,变量的定义需要遵从以下规则。

1、变量名可以包括字母、数字和下划线。

2、变量名必须以字母或下划线开头。

3、变量名区分大小写。

4、避免使用C#的关键字作为变量名。关键字通常为蓝色高亮显示

C#变量的作用域

C#中变量定义在不同位置,其作用区域不一样,不同的作用域变量不能直接访问。

局部变量

在方法、循环、条件语句等代码块内声明的变量是局部变量,它们只在声明它们的代码块中可见。

void MyMethod()
{
    int localVar = 10; // 局部变量
    // ...
    for (int i = 0; i < 5; i++)
    {
    // i 在循环体内可见
    }
    // i 在这里不可见
}
// localVar 在这里不可见

全局变量

在类的成员级别定义的变量是成员变量,它们在整个类中可见,如果在命名空间级别定义,那么它们在整个命名空间中可见。

namespace YourClass
{
    int number=0;       //全局变量,在整个命名空间有效
    class MyClass
    {
    int memberVar = 30; // 成员变量,在整个类中可见
    }
}

C#判断

/*if的使用与C中一致*/
if(判断条件)   /*需要注意的是等于为==,不等于为!=*/
{
    /*if()
    {
    
    }
    else
    {

    } 可嵌套使用 */

}
else if()
{

}
else
{
    /*该else为 else if 的else*/

}

if判断语句还有一个简化的条件运算符

Exp1 ? Exp2 : Exp3;

其中,Exp1、Exp2 和 Exp3 是表达式。请注意,冒号的使用和位置。

? 表达式的值是由 Exp1 决定的。如果 Exp1 为真,则计算 Exp2 的值,结果即为整个 ? 表达式的值。如果 Exp1 为假,则计算 Exp3 的值,结果即为整个 ? 表达式的值。

/*另外还有case语句的用法*/

swtich (判断状态变量){
     case 1: 
            ....
            break;
     case 2: 
            ....
            break;
     case 3: 
            ....
            break;
}

需要注意的是C#的Switch语句需要遵从如下规则

1、C/C++中运行case语句中没有braek,而C#中要求,每个case语句背后必须包括显示的跳转语句 如下 braek,return,go to case。

2、switch语句中的判断状态变量,必须是一个整型或者枚举类型,或者是一个class类型,其中class有一个单一的转换函数将其转换为整型或枚举类型。而C/C++对此没有要求,可以为字符型等其他类型。

C#循环

C#中While循环

while(condition)
{
   statement(s);
}

在这里,statement(s) 可以是一个单独的语句,也可以是几个语句组成的代码块。condition 可以是任意的表达式,当为任意非零值时都为真。当条件为真时执行循环。

当条件为假时,程序流将跳过循环执行,while的下一条语句,而不是执行while循环中的语句。

C#中的for/foreach循环

for ( init; condition; increment )
{
   statement(s);
}


for(int i=1;i<10;i++)
{
   Console.WriteLine(i);
}

1、init会被首先执行且只执行一次,在这里可以用于声明并初始循环控制变量.

2、接下来,会先判断condition。如果为真,则执行statement(s),如果为假则结束循环,跳到for循环的下一跳语句。

3、在执行完statement(s)后,控制流会跳回并执行上面的increment。

4、执行完上面的increment后,再次判断condition,如果为真则继续循环。

由于编程者经常使用fou循环遍历数组元素,因此C# 支持 foreach 循环,C# 的 foreach 循环可以用来遍历集合类型,例如数组、列表、字典等。它是一个简化版的 for 循环,使得代码更加简洁易读。

foreach (var item in collection)
{
    // 循环
}

item是当前遍历到的元素,collection是需要遍历的数组

using System;
using System.Xml.Linq;

namespace this_main
{
    class this_main_1
    {
        static void Main(string[] args)
        {
            int[] fibarray = new int[] { 5, 2, 0, 1, 3, 1, 4 };
            //使用foreach 遍历数组的每个元素并输出 结果为 5 2 0 1 3 1 4
            foreach (var element in fibarray)
            {
                Console.WriteLine(element);
            }
            Console.WriteLine();
            //使用for循环遍历数组的每个元素并输出,结果为 5 2 0 1 3 1 4
            for (int i = 0; i < fibarray.Length; i++)
            {
                Console.WriteLine(fibarray[i]);
            }
            //使用foreach遍历数组的每个元素,并使用count计算数组内元素的数量
            int count = 1;
            foreach (var element in fibarray)
            {
                
                Console.WriteLine("ordinal number:{0} elment:{1}",count,element);
                count++;
            }
            Console.WriteLine("Number of element in the array:{0}",count);
        }
    }
}

C# do...while 循环

不像 for 和 while 循环,它们是在循环头部测试循环条件。do...while 循环是在循环的尾部检查它的条件。

do...while 循环与 while 循环类似,但是 do...while 循环会确保至少执行一次循环。

do
{
   statement(s);

}while( condition );

请注意,条件表达式出现在循环的尾部,所以循环中的 statement(s) 会在条件被测试之前至少执行一次。

如果条件为真,控制流会跳转回上面的 do,然后重新执行循环中的 statement(s)。这个过程会不断重复,直到给定条件变为假为止。

using System;

namespace Loops
{
    
    class Program
    {
        static void Main(string[] args)
        {
            /* 局部变量定义 */
            int a = 10;

            /* do 循环执行 */
            do
            {
               Console.WriteLine("a 的值: {0}", a);
                a = a + 1;
            } while (a < 20);

            Console.ReadLine();
        }
    }
}

当上面的代码被编译和执行时,它会产生下列结果:

a 的值: 10
a 的值: 11
a 的值: 12
a 的值: 13
a 的值: 14
a 的值: 15
a 的值: 16
a 的值: 17
a 的值: 18
a 的值: 19

循环控制语句

循环控制语句更改执行的正常序列。当执行离开一个范围时,所有在该范围中创建的自动对象都会被销毁。

C# 提供了下列的控制语句。

break语句 终止 循环 或 switch 语句,程序流将继续执行紧接着 循环 或 switch 的下一条语句。
continue 跳过本轮循环,开始下一轮循环。

C#封装

C#封装被定义为“把一个或多个项目封闭在一个物理的或者逻辑的包中”。在面向对象程序设计方法论中,封装是为了防止对实现细节的访问。

需要注意的是访问修饰词,只能用来修饰class类成员(class、方法、成员变量等),不能在方法中修饰。

//案例
using System;

namespace this_main
{
    public int a=0;  //❌ ,这不是class类成员
    public class This_main_1      //✅,这属于class类成员
    {
        public int b=0;             //✅,这属于class类成员
        static int Helloworld(int i)  //✅,这属于class类成员
        {
           public int c=0;  //❌ ,这不是class类成员
        }
        static void Main()
        {

        }
    }
}
/*输出结果为
这是实参b:10
这是形参i:11
*/

C#封装根据具体的需要,设置使用者的访问权限,并通过访问修饰符来实现。

这里用程序和示意图的形式来逐个理解,以下所有的成员都处于同一个namespace RectangleApplication 下,不存在不同命名空间的情况,在不同命名空间的情况下,会在后面进行解释。

Public 访问修饰符

using System;

namespace RectangleApplication
{
    class Rectangle
    {
        //成员变量
        public double length;
        public double width;

        public double GetArea()
        {
            return length * width;
        }
        public void Display()
        {
            Console.WriteLine("长度: {0}", length);
            Console.WriteLine("宽度: {0}", width);
            Console.WriteLine("面积: {0}", GetArea());
        }
    }// Rectangle 结束

    class ExecuteRectangle
    {
        static void Main(string[] args)
        {
            Rectangle r = new Rectangle();
            r.length = 4.5;
            r.width = 3.5;
            r.Display();
            Console.ReadLine();
        }
    }
}

该例中,class Rectangle在class ExecuteRectangle被实例化为r

该实例化r中的成员变量length、width均可在class ExecuteRectangle被访问和修改

Private 访问修饰符

using System;

namespace RectangleApplication
{
    class Rectangle
    {
        // 私有成员变量
        private double length;
        private double width;

        // 公有方法,用于从用户输入获取矩形的长度和宽度
        public void AcceptDetails()
        {
            Console.WriteLine("请输入长度:");
            length = Convert.ToDouble(Console.ReadLine());
            Console.WriteLine("请输入宽度:");
            width = Convert.ToDouble(Console.ReadLine());
        }

        // 公有方法,用于计算矩形的面积
        public double GetArea()
        {
            return length * width;
        }

        // 公有方法,用于显示矩形的属性和面积
        public void Display()
        {
            Console.WriteLine("长度: {0}", length);
            Console.WriteLine("宽度: {0}", width);
            Console.WriteLine("面积: {0}", GetArea());
        }
    }//end class Rectangle    

    class ExecuteRectangle
    {
        static void Main(string[] args)
        {
            // 创建 Rectangle 类的实例
            Rectangle r = new Rectangle();

            // 通过公有方法 AcceptDetails() 从用户输入获取矩形的长度和宽度
            r.AcceptDetails();

            // 通过公有方法 Display() 显示矩形的属性和面积
            r.Display();

            Console.ReadLine();
        }
    }
}

该例中,class Rectangle在class ExecuteRectangle被实例化为r

该实例化r中的成员变量length、width不能在class ExecuteRectangle中被访问和修改

只能使用公有方法对成员变量进行修改访问。

打个比方,实例化r为你的银行卡,r中一个变量,我们称之为余额,余额为私有成员变量,不能被外部直接访问修改,只可以通过某些方法、接口修改余额变量,例如纸币存取、ATM等

之所以衍生出访问修饰符,因为程序设计者希望类中的某些变量被保护起来,不能被外部直接修改和访问,这有助于变量的保护和安全。

注意事项:

1、如果一个class被Private修饰,那么即便它内部成员为public,那么其他class也不能访问。因为class本身对外不可见。

Internal 访问修饰符

该访问修饰符修饰后,只能在同一个程序集的对象可以访问。

我们先解释程序集的概念。

程序集是 .NET 中代码编译、版本控制和部署的基本单位。简单来说,它就是你编译项目后生成的那个文件——要么是 .exe(可执行程序),要么是 .dll(类库)。

代码示例说明程序集概念

假设你有两个项目:

项目 A:类库(ClassLibrary.dll)

// ClassLibrary.dll
namespace MyLibrary
{
    public class PublicClass
    {
        public void Hello() { }
    }
    
    internal class InternalClass
    {
        public void Secret() { }
    }
}

项目 B:控制台应用(ConsoleApp.exe),引用了 ClassLibrary.dll

// ConsoleApp.exe
using MyLibrary;

class Program
{
    static void Main()
    {
        var pub = new PublicClass();     // ✅ 可以,PublicClass 是 public
        pub.Hello();                     // ✅ 可以
        
        // var inter = new InternalClass(); // ❌ 编译错误,InternalClass 是 internal
        // 因为它在另一个程序集(ClassLibrary.dll)中,而 internal 只在本程序集内可见
    }
}

Protected、Protected Internal 访问修饰符

这两个修饰符将在后面的继承中提到。

C#数组

数组是一个存储相同类型元素的固定大小的顺序集合。数组是用来存储数据的集合,通常认为数组是一个同一类型变量的集合。

声明数组变量并不是声明 number0、number1、...、number99 一个个单独的变量,而是声明一个就像 numbers 这样的变量,然后使用 numbers[0]、numbers[1]、...、numbers[99] 来表示一个个单独的变量。数组中某个指定的元素是通过索引来访问的。

所有的数组都是由连续的内存位置组成的。最低的地址对应第一个元素,最高的地址对应最后一个元素。

声明数组

在 C# 中声明一个数组,您可以使用下面的语法:

datatype[] arrayName;

datype为值类型,arrayName为数组名称

初始化数组

声明一个数组不会在内存中初始化数组。当初始化数组变量时,您可以赋值给数组。

数组是一个引用类型,所以您需要使用 new 关键字来创建数组的实例。

例如:

//可以先声明,后使用new创建

int []arrayName;

arrayName =new int[10]{ 1 , 2 ,3 };

//也可以声明并初始化

 int[] arraayName = new int[10] ;

赋值给数组

您可以通过使用索引号赋值给一个单独的数组元素,比如:

int[] arrayName=new int[10];

arrayName[0]=4500;

您可以在声明数组的同时给数组赋值,比如:

int[] arrayName={1,2,3};

您也可以声明并初始化一个数组,比如:

int [] arrayName=new int[3]{1,2,3};

在上述情况下,你也可以省略数组的大小,比如:

int [] arrayName=new int[]{1,2,3};

您也可以赋值一个数组变量到另一个目标数组变量中。在这种情况下,目标和源会指向相同的内存位置:

int [] marks = new int[]  { 99,  98, 92, 97, 95};
int[] score = marks;

当您创建一个数组时,C# 编译器会根据数组类型隐式初始化每个数组元素为一个默认值。例如,int 数组的所有元素都会被初始化为 0。

数组参数

假设我需要把一个数组传入方法中,并由方法进行数据处理后打印

//案例
using System;
using System.Diagnostics.Eventing.Reader;

namespace this_main
{
    class Program
    {
        static void Main()
        {
            int[] _main_array = new int[] { 1, 2, 3 };
            int sum = arrayName_metod(_main_array);
            Console.WriteLine(sum); 
        }
        static int arrayName_metod(int[] array)
        {
            int sum = 0;
            foreach (var item in array)      //遍历数组并累加
            {
                sum = sum + item;
            }
            return sum;
        }
    }
}
 //结果输出为6

在该案例中,我们需要先声明并初始化一个数组后,传入方法arrayName_metod进行数据处理,这里就比较麻烦,微软提供了params关键字。在方法构造时直接使用params关键字便可以不用声明数组,直接代入数组数据

//案例
using System;
using System.Diagnostics.Eventing.Reader;

namespace this_main
{
    class Program
    {
        static void Main()
        {
            int sum = arrayName_metod(1,2,3);     //直接代入数组数据
            Console.WriteLine(sum); 
        }
        static int arrayName_metod(params int[] array)  //params关键字修饰
        {
            int sum = 0;
            foreach (var item in array)      //遍历数组并累加
            {
                sum = sum + item;
            }
            return sum;
        }
    }
}
 //结果输出为6

需要注意的是方法中的数组参数必须是形参参数表中的最后一个,由params修饰。

C#进阶内容

至此,C#的入门内容基本写完了,其中有部分例如算术运算符、逻辑运算符、常量、操作符等一小部分知识,因为与C是一致的,因此在这里我就不一一详细讲了。

C#委托

什么是委托?

在C中我们可以使用指针去传递地址,从而在多个函数操纵同一个变量。但是在C#中,指针被认为是不安全的代码,因此C#设计了一系列方法去避免使用指针,但是实现指针的功能。

例如函数实参形参传递避免使用指针,从而有按引用传递ref、按输出传递out

在函数指针中,C#构造了委托这么一个东西,去代替函数指针。

1、委托(delegate)是函数指针的"升级版"

2、一切都是地址

变量(数据)是保存在机器内存中的一段地址中,那么函数(算法)其实也有一段地址,在方法参数传递中,我们知道了out、ref相当于将内存地址传递给方法,那么委托与其类似,是将方法的内存地址传递给某个地方进行使用。

3、直接调用与间接调用

直接调用:通过函数名来调用函数,CPU通过函数名直接获得函数所在地址并开始执行->返回

间接调用:通过函数指针来调用函数,CPU通过读取函数指针存储的值获得函数所在地址并开始执行->返回

委托的简单使用

Action委托

Func委托

Action委托

用于指向无形参、无返回值的方法

其声明方法为

Action 委托名 =new Action(类.方法)

//案例
using System;
using System.Diagnostics.Eventing.Reader;

namespace this_main
{
    class Program
    {
        static void Main()
        {
            Calculator calculator = new Calculator();
            Action action = new Action(calculator.Report);
            calculator.Report();    //直接调用calculator.Report
            action.Invoke();        //间接通过action.Invoke调用
            action();               //间接调用,仿函数指针用法
        }
    }
  class Calculator

    {
        public void Report()                //无返回值、无形参的方法
        {
            Console.WriteLine("我有三个方法");
        }
      }
}
/*结果输出
我有三个方法
我有三个方法
我有三个方法
*/

Fuction委托

针对有返回值,有形参的方法,我们采用Fuction委托

其声明方法为

Func<参数类型表,返回值>委托名=new Fun<<参数类型表,返回值>>(类.方法)

注意:VS输入时Func具有17个重载,重载可以将鼠标放入<>中选择,我们选择第三个个重载,该重载具有两个参数传递,一个返回值

//案例
using System;
using System.Diagnostics.Eventing.Reader;

namespace this_main
{
    class Program
    {
        static void Main()
        {
            Calculator C1 = new Calculator();
            Func<int, int, int> func1 = new Func<int, int, int>(C1.Add);
            Func<int,int,int> func2 = new Func<int, int, int>(C1.Sub);
            int a = 100;
            int b = 50;
            int c = 0;
            c=func1.Invoke(a, b);
            Console.WriteLine(c);   
            c=func2.Invoke(a, b);   
            Console.WriteLine(c);
        }
    }
  class Calculator

    {
        public void Report()
        {
            Console.WriteLine("我有三个方法");
        }
        public int Add(int a, int b)
        {
            int result = a + b;
            return result;
        }
        public int Sub(int a, int b)
        {
            int result = a - b;
            return result;
        }
            
    }
}
/*结果输出
150
50
*/

自定义委托的声明

讲解了 C# 自带的Action和Function两种委托的用法和声明,现在来具体讲一讲自定义委托的声明

首先委托是一种类(class),类是数据类型所以委托也是一种数据类型。

它的声明方式与一般的类不同

声明方式

public delegate 返回类型 委托名(参数类型 参数名, ...);

例如以下代码,我们定义一个接受两个整数并返回一个整数的委托:

public delegate int MathOperation(int x, int y);

与类平级,需要声明在命名空间下

如果不小心写在了类里面,就变成了嵌套类,编译器也可以通过,程序也可以正常运行,但有风险

/*案例代码*/
using System;

namespace helloword
{
    public delegate int Calc(int x, int y);  //自定义声明委托
    internal class Program
    {
        public delegate int class_Cacl(int x, int y);  //嵌套类自定义声明委托
        static void Main(string[] args)
        {
        
        }
    }
  
}
/*案例代码*/
using System;

namespace helloword
{
    public delegate int Calc(int x, int y);  //自定义声明委托
    internal class Program
    {
        static void Main(string[] args)
        {
            int x = 10;
            int y = 20;
            Program_2 program_2=new Program_2();        //实例化Program_2
            Calc calc_1 = new Calc(program_2.Add);      //实例化自定义委托calc_1
            Calc calc_2 = new Calc(program_2.Sub);      //实例化自定义委托calc_2
            Console.WriteLine(calc_1.Invoke(x,y));      //Invoke调用
            Console.WriteLine(calc_1(x,y));             //函数指针用法调用
            Console.WriteLine(calc_2.Invoke(x, y));     //Invoke调用
            Console.WriteLine(calc_2(x, y));            //函数指针用法调用
        }
    }
    public class Program_2
    {
        public int Add(int a,int b)
        {
            return a + b; 
        }
        public int Sub(int a, int b)
        {
            return a - b;
        }
    }
}
/*输出结果
 *30
-10
 */

模板方法

什么是模板方法?
模板方法是委托的应用方式之一。就像做菜:无论做什么菜,流程都是“洗菜 → 切菜 → 烹饪 → 装盘”。但“烹饪”这一步不同:红烧肉用炖,炒青菜用炒。

那么我们就可以构建一个模板方法,其中包含洗菜切菜 装盘的固定流程,至于烹饪这个可变流程就可以交给委托去完成。

/*案例代码*/
using System;

namespace helloword
{
    internal class Program
    {
        static void Main(string[] args)
        {
            CookDish("青菜", chao);
            CookDish("红烧肉", dun);

        }
        /*定义一个模板方法,标准做菜流程,唯一的区别是菜不同
         * 因此有两个输入,dishName为菜的名字,委托Funtion cookMethod为不同菜时的烹饪做法,有一个string类型的参数和int返回值*/
        static void CookDish(string cookName, Func<string,int> cookMethod)

        {
            Console.WriteLine("1. 洗菜");
            Console.WriteLine("2. 切菜");
            cookMethod(cookName);
            Console.WriteLine("4. 装盘");
            Console.WriteLine("-------------------");
        }
        //定义具体的烹饪方法
        //炖
        static int dun(string Name)
        {
            Console.WriteLine("3.把{0}放进锅里炖30分钟", Name);
            return 0;
        }
        //炒
        static int chao(string Name)
        {
            Console.WriteLine("3.把{0}放进锅里炒5分钟", Name);
            return 0;
        }
    }
}

结果输出

1. 洗菜
2. 切菜
3.把青菜放进锅里炒5分钟
4. 装盘
-------------------
1. 洗菜
2. 切菜
3.把红烧肉放进锅里炖30分钟
4. 装盘
-------------------

回调方法

什么是回调方法?
就像你去银行办理业务:你把银行卡交给柜台,然后去逛街。等业务办好了,工作人员打电话通知你来取。这个“打电话通知”就是回调
回调方法就是:你先告诉程序“事情做完后,请调用这个方法通知我”。

/*案例代码*/
using System;

namespace helloword
{
   public class Program
    {
        static void Main()
        {
            //1、创建一个办事员类
           Clerk clerk = new Clerk();
            //办理业务
            Console.WriteLine("提交银行卡");
            Console.WriteLine("我先去逛下街");
            //办理完成后通过这个CallMe委托方法提醒我HandleBuiness方法执行完成
            clerk.HandleBuiness(CallMe);  
        
        }
        static void CallMe()
        {
            Console.WriteLine("办理完成,请回来取银行卡");
         }
     }
    public class Clerk
        {
        //业务办理过程
        public void HandleBuiness(Action Callback)
        {
            Console.WriteLine("正在办理业务");
            // 模拟耗时操作(用 Sleep 代替,只是为了演示)
            System.Threading.Thread.Sleep(2000); // 暂停2秒
            Callback();  //执行委托函数
        }
        }

}

结果输出

提交银行卡
我先去逛下街
正在办理业务
办理完成,请回来取银行卡

无论是模板方法还是回调方法,本质上其实都是将委托作为参数输入进方法中,并在方法中跳转至委托方法并执行委托方法。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐