第一节:再探C#语法(顶级语句、全局using、namespace、using管理、可空类型、record新类型)

一. 顶级语句

1. 直接在C#文件中直接编写入口方法的代码,不用类,不用Main.同时经典写法仍然支持,反编译一下了解真相.  
2. 同一个项目中只能有一个文件具有顶级语句
3. 顶级语句中可以直接使用await语法,也可以声明函数

代码分享:

Console.WriteLine("测试写入文件哦");
await File.WriteAllTextAsync("1.txt", "hello");
Console.WriteLine("写入完成");
Console.ReadKey();

反编译代码:

 

二. global using

1. 说明

   将 global 修饰符添加到 using 前,这个命名空间就应用到整个项目,不再需要重复每个文件使用using了。

2. 常用套路

   通常创建一个专门用来编写全局using代码的C#文件。

   比如新建一个GlobalUsing.cs类,里面使用global using 导入需要使用的命名空间,那么在该项目中的其它文件使用这些命名空间下的文件方法的时候,不再需要using了

3. 补充

   如果csproj中启用了<ImplicitUsings>enable</ImplicitUsings>,编译器会自动隐式增加对于System、System.Linq等常用命名空间的引入,不同各类型项目引入的命名空间也不一样。

 

三. 文件范围的声明空间

   比如新建1个类,它都是在 namespace xxx { 类xxx},现在可以省略最外层的{},详见 utils/TestHelp2

旧写法:

namespace _02_基础总结.utils
{
    internal class TestHelp2
    {
    }
}

新写法:

namespace _02_基础总结.utils;
internal class TestHelp2
{
}

 

四. using管理

1. 旧写法

  实现了IDisposible接口的对象可以用using进行管理, 形如 using(var xxx= new xxx){},  其中 {} 执行完毕,using中的内容dispose释放。

 string connStr = "Data Source=.;Initial Catalog=demo1;Integrated Security=True";
    using (var conn = new SqlConnection(connStr))
    {
        conn.Open();
        using (var cmd = conn.CreateCommand())
        {
            cmd.CommandText = "select * from T_Articles";
            using (SqlDataReader reader = cmd.ExecuteReader())
            {
                while (reader.Read())
                {
                }
            }
        }
    }

自己实现一个案例测试: 

  using (MyFile myFile = new MyFile())
    {
        Console.WriteLine("执行完毕");
    }

运行结果:

 

2. 新写法(C#8.0)

  在实现了Idisposable/IAsyncDisposable接口的类型的变量声明前加上using,当代码执行离开变量的作用域时(比如这些代码在一个函数里,那么就是离开这个函数),对象就会被释放。

  形如:using SqlConnection conn = new SqlConnection("xxx") ; 当离开conn的作用域的时候,conn将被释放 如下案例

详细测试:using后面变量离开作用域释放问题

前置代码:MyFile类

public class MyFile : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("MyFile被释放了");
    }
}

(1). 案例1--using作用域为所在的函数

    myFile的作用域为TestDispose所在的函数,当函数结束,myFile被释放,所以运行结果如下:

// 测试1--using作用域为所在的函数
{
    Console.WriteLine("start");
    TestDispose();
    Console.WriteLine("end");


    static void TestDispose()
    {
        using MyFile myfile = new MyFile();
        Console.WriteLine("TestDispose执行完毕");
    }

}

 

 (2). 案例2--using作用域是为每次for循环结束 

    myFile的作用域为所在的for循环每次结束的时候释放,所以运行结果如下

// 测试2--using作用域为每次for循环结束
{
    Console.WriteLine("start");
    TestDispose();
    Console.WriteLine("end");

    static void TestDispose()
    {
        for (int i = 0; i < 2; i++)
        {
            using MyFile myfile = new();
            Console.WriteLine($"i={i}");
        }
        Console.WriteLine("TestDispose执行完毕");
    }
}

 

3. using声明陷阱

  如下例子,使用新的using写法,导致上面写文件的using还没释放,就又要去读文件,所以会报错

System.IO.IOException: The process cannot access the file 'e:\1.txt' because it is being used by another process.

//using陷阱
/*
 System.IO.IOException: The process cannot access the file 'e:\1.txt' because it is being used by another process.
 */
{
    //写文件
    using var outStream = File.OpenWrite("e:/1.txt");
    using var writer = new StreamWriter(outStream);
    writer.WriteLine("hello");
    //读文件
    string s = File.ReadAllText("e:/1.txt");
    Console.WriteLine(s);

}

  解决方案:把上面写的using释放到即可

  方案1:using包裹的模式

  方案2:给using新写法加1层作用域

//方案1-using包裹的模式
{
    using (var outStream = File.OpenWrite("e:/1.txt"))
    using (var writer = new StreamWriter(outStream))
    {
        writer.WriteLine("hello");
    }
    string s = File.ReadAllText("e:/1.txt");
    Console.WriteLine(s);
}
//方案2-给using新写法加1层作用域
{
    {
        using var outStream = File.OpenWrite("e:/1.txt");
        using var writer = new StreamWriter(outStream);
        writer.WriteLine("hello");
    }
    string s = File.ReadAllText("e:/1.txt");
    Console.WriteLine(s);

}

五. 可空引用类型

1. 背景

  C#数据类型分为值类型和引用类型两种,值类型的变量不可以为空,而引用类型变量可以为空。

  问题:如果不注意检查引用类型变量是否可空,就有可能造成程序中出现NullReferenceException异常。

2. 实操

  (1). csproj中<Nullable>enable</Nullable>启用可空引用类型检查

  (2). 在引用类型后添加“?”修饰符来声明这个类型是可空的。对于没有添加“?”修饰符的引用类型的变量,如果编译器发现存在为这个变量赋值null的可能性的时候,编译器会给出警告信息

  如:Nullable/student类中的name 和 array 是引用类型,需要加个?, 否则报警告。

 internal class Student
    {
        public int Id { get; set; }
        public string? Name { get; set; }

        public DateTime CreatedDate { get; set; }

        public string[]? array { get; set; }

    }

  (3). 如果程序员确认被访问的变量、成员确实不会出现为空的情况,也可以在访问可空的变量、成员的时候加上!来抑制编译器的警告。

   Console.WriteLine(s1.PhoneNumber!.ToLower());

  (4). 个人倾向:不喜欢开启Nullable

 

六. record新类型

1. 背景

   C#中的==运算符 和 equal方法 默认是判断两个变量指向的是否是同一个对象,即使两个对象内容完全一样,也不相等。可以通过重写Equals方法、重写==运算符等来解决这个问题,不过需要开发人员编写非常多的额外代码。

代码分享:

 public class Person1
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public Person1(int Id,string Name)
        {
            this.Id = Id;
            this.Name = Name;
        }
    }

测试:

Person1 p1 = new Person1(1, "ypf1");
Person1 p2 = new Person1(1, "ypf1");
Person1 p3 = p1;

Console.WriteLine(p1 == p2);  //false
Console.WriteLine(p1.Equals(p2)); //false

//下面指向同一个地址
Console.WriteLine(p1 == p3);  //true
Console.WriteLine(p1.Equals(p3)); //true

2. record

  在C#9.0中增加了记录(record)类型的语法,编译器会为我们自动生成Equals、GetHashcode等方法。

  编译器会根据Person类型中的属性定义,自动为Person类型生成包含全部属性的构造方法。注意,默认情况下,编译器会生成一个包含所有属性的构造方法,

  因此,我们编写new Person()、new Person(“ypf”)这两种写法都是不可以的。也会生成ToString方法和Equals等方法

  对于record类型,如果两个两个对象内容完全一样,使用equal或==相比较,返回的是true。如Person2相关案例

代码分享

 public record Person2(int Id,string Name);

测试

{
    Person2 p1 = new(1, "ypf1");
    Person2 p2 = new(1, "ypf1");
    Console.WriteLine(p1 == p2);  //true
    Console.WriteLine(p1.Equals(p2)); //true
}

 3. 补充

   默认生成的构造方法的行为不能修改,我们可以为类型提供多个构造方法,然后其他构造方法通过this调用默认的构造方法。

   如下Person3案例

4. 对象的拷贝

   record也是普通类,变量的赋值是引用的传递。

   生成一个对象的副本,这个对象的其他属性值与原对象的相同,只有一个或者少数几个属性改变。麻烦的做法:User u2 = new User(u1.UserName, "test@example", u1.Age);

   简单的方法:用with关键字简化:User u2 = u1 with { Email= “test@example” }; 创建的也是拷贝

   特别注意:with创建的对象相当于拷贝,指向不同的地址。

代码分享:

 public record Person3(int Id, string Email, string Name)
    {
        public Person3(int Id, string Name) : this(Id, null, Name)
        {

        }
    }

测试:

{
    Person3 p1 = new(1, "ypf1");
    Person3 p2 = new(1, "ypf@qq.com", "ypf2");


    Person3 p3 = p1 with { };
    Console.WriteLine(p1 == p3); //true
    Console.WriteLine(Object.ReferenceEquals(p1, p3)); //比较两个对象是否指向同一个地址, false

    Person3 p4 = p1 with { Email = "lmr@qq.com" };
    Console.WriteLine(p1 == p4); //false
    Console.WriteLine(Object.ReferenceEquals(p1, p4)); //比较两个对象是否指向同一个地址, false
}

5. 用做接口的参数接收

   可以用作参数的实体接收,但是赋默认值无效。

实体:

/// <summary>
/// 使用record类型声明实体
/// </summary>
/// <param name="userAccount"></param>
/// <param name="userPwd"></param>
public record LoginModel(string userAccount,string userPwd);

api接口:

   [HttpPost]
    public string CheckLogin(LoginModel model)
    {
        return "ok," + model.userAccount + "," + model.userPwd;
    }

 

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 

 

posted @ 2022-05-07 14:40  Yaopengfei  阅读(2755)  评论(2编辑  收藏  举报