n1cef1sh's Blog

前记

作为平时的学习记录,内容大多来自微软官方文档以及《C#图解教程》(第五版)。

2020.8.28

C#字符串

1.字符串内插
在字符串左引号前添加$,可以在大括号 之间的字符串内包括变量。例如

Console.WriteLine($"Hello {aFriend}");

2.几个方法
TrimStart、TrimEnd删除空格
Replace替换
Contains搜索包含 StartWith、EndWith搜索开头结尾

VisualStudio基础使用

QQ截图20200829211204.png

QQ截图20200828160448.png 项目中使用using引进包,JsonConvert.SerializeObject方法将对象转换为可人工读取的字符串。

控制台应用Calculator

简单的创建WPF应用程序

简单的创建Windows窗体应用程序

与之前的简单安卓app类似。

2020.9.2

简单的窗体应用

计时数学测验没啥好说的。
匹配游戏MatchingGames

字符串内插的高级方案

var userName = "pyq";
var stringWithEscapes = $"C:\\Users\\{userName}\\Documents";
var verbatimInterpolated = $@"C:\Users\{userName}\Documents";
Console.WriteLine(stringWithEscapes);
Console.WriteLine(verbatimInterpolated);
// C:\Users\pyq\Documents
// C:\Users\pyq\Documents
var cultures = new System.Globalization.CultureInfo[]
{
    System.Globalization.CultureInfo.GetCultureInfo("en-US"),
    System.Globalization.CultureInfo.GetCultureInfo("en-GB"),
    System.Globalization.CultureInfo.GetCultureInfo("nl-NL"),
    System.Globalization.CultureInfo.InvariantCulture
};

var date = DateTime.Now;
var number = 31_415_926.536;
FormattableString message = $"{date,20}{number,20:N3}";
foreach (var culture in cultures)
{
    var cultureSpecificMessage = message.ToString(culture);
    Console.WriteLine($"{culture.Name,-10}{cultureSpecificMessage}");
}

// Expected output is like:
// en-US       5/17/18 3:44:55 PM      31,415,926.536
// en-GB      17/05/2018 15:44:55      31,415,926.536
// nl-NL        17-05-18 15:44:55      31.415.926,536
//            05/17/2018 15:44:55      31,415,926.536

索引和范围

0 索引与 sequence[0] 相同。 ^0 索引与 sequence[sequence.Length] 相同。 表达式 sequence[^0] 不会引发异常,就像 sequence[sequence.Length] 一样。 对于任何数字 n,索引 ^n 与 sequence[sequence.Length - n] 相同。

string[] words = new string[]
{
                // index from start    index from end
    "The",      // 0                   ^9
    "quick",    // 1                   ^8
    "brown",    // 2                   ^7
    "fox",      // 3                   ^6
    "jumped",   // 4                   ^5
    "over",     // 5                   ^4
    "the",      // 6                   ^3
    "lazy",     // 7                   ^2
    "dog"       // 8                   ^1
};              // 9 (or words.Length) ^0

类型系统

CTS(通用类型系统)中值类型和引用类型 QQ截图20200902170229.png

int i = 123;      // a value type
object o = i;     // boxing
int j = (int)o;   // unboxing

QQ截图20200902183902.png

装箱时生成的是全新的引用对象,会有时间损耗,造成效率降低。所以应尽量避免装箱。一般可通过重载函数和泛型避免。

委托(Delegate)

建议不要直接在全局命名空间中声明委托类型(或其他类型)。

委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序具有更好的可扩展性。
使用委托可以将多个方法绑定到同一个委托变量,当调用此变量时(这里用“调用”这个词,是因为此变量代表一个方法),可以依次调用所有绑定的方法。
声明和定义委托分为4个步骤:

1.声明一个委托(函数指针)
2.创建委托对象
3.创造符合委托格式的函数。(指针指向的函数)
4.将函数名称赋值给委托。

更详细的 解释和实例

事件Event

事件是后期绑定机制,事件是对象用于(向系统中的所有相关组件)广播已发生事情的一种方式。 任何其他组件都可以订阅事件,并在事件引发时得到通知。
定义事件

public event EventHandler<FileLisArgs> Progress;

引发事件时,使用委托调用语法调用事件

Process?.Invoke(this, new FileLisArgs(file));
//?. 运算符可确保在事件没有订阅服务器时不引发事件。

使用 += 运算符订阅事件, -=取消订阅。

9.3

变量

参数

值参数

class Program
        {
            static void MyMethod(MyClass f1, int f2)
            {
                f1.Val = f1.Val + 5;
                f2 = f2 + 5;
                Console.WriteLine($"f1.Val:{f1.Val}, f2:{f2}");
            }

            static void Main()
            {
                MyClass a1 = new MyClass();
                int a2 = 10;
                MyMethod(a1, a2);
                Console.WriteLine($"a1.Val:{a1.Val},a2:{a2}");
            }
        }
//f1.Val:25, f2:15
//a1.Val:25, a2:10

引用参数

//f1.Val:25, f2:15
//a1.Val:25, a2:15

引用类型作为值参数和引用参数

class MyClass
        {
            public int Val = 20;
        }

        class Program
        {
            static void RefAsParameter(MyClass f1)
            {
                f1.Val = 50;
                Console.WriteLine($"After member assignment: {f1.Val}");
                f1 = new MyClass();
                Console.WriteLine($"After new object creation: {f1.Val}");
            }

            static void Main()
            {
                MyClass a1 = new MyClass();
                Console.WriteLine($"Before Method call: {a1.Val}");
                RefAsParameter(a1);
                Console.WriteLine($"After method call: {a1.Val}");
            }
        }
//Before Method call: 20
//After member assignment: 50
//After new object creation: 20
//After method call: 50
Before Method call: 20
After member assignment: 50
After new object creation: 20
After method call: 20

输出参数

和引用参数不同的地方

//原来的写法
 static void Main()
            {
                MyClass a1 = new MyClass();
                int a2;
                MyMethod(out a1, out a2);
            }
//新的写法
static void Main()
            {
                MyMethod(out MyClass a1, out int a2);
            }

参数数组

9.5

属性

    private int firstField;
    //或写成_firstField
    public  int FirstField;

readonly修饰符

索引器

分部方法

9.6

类和继承

屏蔽

覆写

析构函数执行

    public MyDerivedClass(int x, string s):base(s,x){
        ……
    }

2、使用this并指明使用当前类的哪一个构造函数

    public MyClass(int x):this(x,"using default string"){
        ……
    }

这种语法的好处是,如果一个类有好几个构造函数,并且都需要在对象构造过程开始时执行一些公共的代码,就可以提取公共代码作为一个公共构造函数(设置为public),被其他所有的构造函数用作构造函数初始化语句。

程序集间的继承

成员访问修饰符

五种访问级别如图所示 蜂蜜浏览器_IMG_20200906_111340.jpg

QQ截图20200906112308.png

扩展方法

    static class ExtendMyData{
        public static double Average(this MyData md){
            ……
        }
    }

表达式和运算符

字面量

    char c1 = "d";//单个字符
    char c2 = "\n";//简单转义序列
    char c3 = "\x0061";//十六进制转义序列
    char c4 = "\u005a";//Unicode转义序列
    

9.7

表达式和运算符

比较操作

条件逻辑运算符

使用“短路”模式操作,如果计算Expr1之后结果已经确定了,将会跳过Expr2的求值。因此不要在Expr2中放置带副作用的表达式(比如改变一个值),因为可能不会计算。

移位运算符

用户定义的类型转换

    public static implicit operator TargetType(SourceType Identifier){
        ……
        return ObjectOfTargetType;
    }

运算符重载

typeof运算符

class SomeClass
    {
        public int Field1;
        public int Field2;

        public void Method1() { }
        public int Method2() { return 1; }
    }

    class Program
    {
        static void Main()
        {
            Type t = typeof(SomeClass);
            FieldInfo[] fi = t.GetFields();
            MethodInfo[] mi = t.GetMethods();

            foreach (FieldInfo f in fi)
                Console.WriteLine($"Field: {f.Name}");
            foreach (MethodInfo m in mi)
                Console.WriteLine($"Method: {m.Name}");
        }
    }

输出结果为

Field: Field1
Field: Field2
Method: Method1
Method: Method2
Method: GetType
Method: ToString
Method: Equals
Method: GetHashCode

GetType方法也会调用typeof运算符,该方法对每个类型的每个对象都有效。

9.8

using语句

第一种形式:括号内代码分配资源;statement时使用资源的代码;using语句隐式产生处置资源的代码。

using(ResourceType Identifier = Expression)Statement

它会执行这些任务:

整个结构被封闭在一个隐式的块里,当finally块退出时,资源失效且不会被意外调用。

第二种形式(不推荐使用):资源在using语句前声明

using(Expression) EmbeddedStatement
        资源           使用资源

数组

基础

交错数组

    int[][] jarArr = new int[3][]; //正确
    int[][] jarArr = new int[3][4];//编译错误
    int[][] Arr = new int[3][];//实例化顶层数组
    
    Arr[0] = new int[]{10,20,30};//实例化子数组
    Arr[1] = new int[]{40,50,60,70};
    Arr[2] = new int[]{80,90,100,110,120};

foreach

    Class MyClass{
        public int MyField = 0;
    }
    
    Class Program{
        static void Main(){
            MyClass[] mcArray = new MyClass[4];
            for(int i = 0; i < 4; i++){
                mcArray[i] = new MyClass[4];
                mcArray[i].MyField = i;
            }
            foreach(myClass item in mcArray)
                item.MyField += 10;
            foreach(MyClass item in mcArray)
                Console.WriteLine($"{item.Field}"));
        }
    }

输出为

10
11
12
13

数组协变

在某些情况下,即使某个对象不是数组的基类型,也可以把它赋值给数组元素。需要符合如下条件:

因此总是可以将一个派生类的对象赋值给基类声明的数组。

Clone方法

9.10

VB基本结构

简单的示例

Imports System

Public Class Rectangle
    Private length As Double
    Private width As Double

    Public Sub AcceptDetails()
        length = 4.5
        width = 3.5
    End Sub

    Public Function GetArea() As Double
        GetArea = length * width
    End Function

    Public Sub Display()
        Console.WriteLine("Length:{0}", length)
        Console.WriteLine("Width:{0}", width)
        Console.WriteLine("Area:{0}", GetArea())
    End Sub

    Shared Sub Main()
        Dim r As New Rectangle
        r.AcceptDetails()
        r.Display()
        Console.ReadLine()
    End Sub

End Class

一个简单VB程序主要的组成部分

9.14

委托(补充)

概述

Lambda表达式

    delegate double MyDel(int par);
    class Program
    {
        static void Main()
        {
           
            MyDel del = delegate (int x) { return x + 1; };//匿名方法

            MyDel le1 = (int x) => { return x + 1; };//Lambda表达式
            MyDel le2 = (x) => { return x + 1; };
            MyDel le3 = x=> { return x + 1; };
            MyDel le4 = x => x + 1;

            Console.WriteLine($"{del(12)}");
            Console.WriteLine($"{le1(12)}");
            Console.WriteLine($"{le2(12)}");
            Console.WriteLine($"{le3(12)}");
            Console.WriteLine($"{le4(12)}");
        }
    }
    //输出都是13

事件

发布pub/订阅sub模式

这是一种经常使用到的设计模式,sub会监听一类消息来达到pub发布的时候进行相应的处理。
看到网上有人把它和观察者模式比较 QQ截图20200914101709.png

从表面上看:

观察者模式里,只有两个角色 —— 观察者 + 被观察者 而发布订阅模式里,还有一个—— 经纪人Broker

往更深层次讲:

观察者和被观察者,是松耦合的关系 发布者和订阅者,则完全不存在耦合

但是实际上,《设计模式》这本书讲观察者模式对复杂情况下如何处理进行了描述:

对于不同观察者需要关心不同的变化时需要加入ObserverManager进行复杂情况的处理。观察者将被观察时间在ObserverManager中进行注册,事件发生变更时在ObserverManager中寻找已注册该事件的观察者,将消息发送给对应的观察者。

ObserverManager就是Broker的角色,其实两个没有本质性的区别。

源代码组件概览

需要在事件中使用的代码有五部分

声明事件

    class Incrementer{
        public event EventHandler CountedADozen;
              关键字  委托类型       事件名
        ……
    }

订阅事件

使用+=运算符添加事件处理程序,事件处理程序的规范可以是以下任意一种

触发事件

完整示例

    delegate void Handler();

//发布者pub
    class Incrementer
    {
        public event Handler CountedADozen;
    //public event EventHandler CountedADozen;
        public void DoCount()
        {
            for(int i = 1; i < 100; i++)
            {
                if (i % 12 == 0 && CountedADozen != null)
                    CountedADozen();
                    //CountedADozen(this,null);
            }
        }
    }
//订阅者sub
    class Dozens
    {
        public int DozensCount { get; private set; }

        public Dozens(Incrementer incrementer)
        {
            DozensCount = 0;
            incrementer.CountedADozen += IncrementDozensCount;
//void IncrementDozensCount(object source, EventArgs e)
            void IncrementDozensCount()
            {
                DozensCount++;
            }
        }
    }

    class Program
    {
        static void Main()
        {
            Incrementer incrementer = new Incrementer();
            Dozens dozensCounter = new Dozens(incrementer);

            incrementer.DoCount();
            Console.WriteLine("Number of dozens = {0}", dozensCounter.DozensCount);
        }
    }
    //输出:Number of dozens = 8

标准事件的用法

System命名空间中声明的EventHandler委托类型
首先上例中注释掉的部分,就是改为使用EventHandler委托的写法。
关于委托的声明:

    public delegate void EventHandler(object sender, EventArgs e);

这其中第二个参数是EventArgs类的对象,但是

object和EventArgs类型的参数总是基类,这样EventHandler就可以提供一个对所有事件和事件处理器都通用的签名。

通过扩展EventArgs来传递数据

对上面的实例进行修改扩展

    delegate void Handler();
    
    public class IncrementerEventArgs: EventArgs
    {
        public int IterationCount { get; set; }
    }
    class Incrementer
    {
        public event EventHandler<IncrementerEventArgs> CountedADozen;
        public void DoCount()
        {
            IncrementerEventArgs args = new IncrementerEventArgs();
            for(int i = 1; i < 100; i++)
            {
                if (i % 12 == 0 && CountedADozen != null)
                {
                    args.IterationCount = i;
                    CountedADozen(this, args);
                }
                    
            }
        }
    }

    class Dozens
    {
        public int DozensCount { get; private set; }

        public Dozens(Incrementer incrementer)
        {
            DozensCount = 0;
            incrementer.CountedADozen += IncrementDozensCount;

            void IncrementDozensCount(object source,IncrementerEventArgs e)
            {
                Console.WriteLine($"Incremented at iteration :{e.IterationCount}in {source.ToString()}");
                DozensCount++;
            }
        }
    }

    class Program
    {
        static void Main()
        {
            Incrementer incrementer = new Incrementer();
            Dozens dozensCounter = new Dozens(incrementer);

            incrementer.DoCount();
            Console.WriteLine("Number of dozens = {0}", dozensCounter.DozensCount);
        }
    }
        
}
输出:
Incremented at iteration :12in demo1.Incrementer
Incremented at iteration :24in demo1.Incrementer
Incremented at iteration :36in demo1.Incrementer
Incremented at iteration :48in demo1.Incrementer
Incremented at iteration :60in demo1.Incrementer
Incremented at iteration :72in demo1.Incrementer
Incremented at iteration :84in demo1.Incrementer
Incremented at iteration :96in demo1.Incrementer
Number of dozens = 8
移除事件处理程序

接口

使用IComparable接口的示例

Array类的Sort()方法依赖于一个叫做IComparable的接口,声明在BCL(基类库Base Class Library)中,包含唯一的方法CompareTo。

    public interface IComparable{
        int CompareTo(object obj);
    }

在IComparable接口的声明中,包含Compare方法的声明,但是没有实现,实现用分号表示。但是. NET文档中描述了该方法应该做的事情。

文档中写道,在调用该方法时,它应该返回以下几个值之一:

int类型实现了IComparable,因此sort方法可以直接排序,但是自己定义的类数组并没有实现IComparable所以直接使用sort的话会出现异常。于是可以让自己的类实现IComparable接口

    class MyClass:IComparable{
        public int TheValue;
        
        public int CompareTo(object obj){
            MyClass mc = (MyClass)obj;
            if(this.TheValue < mc.TheValue)return -1;
            if(this.TheValue > mc.TheValue)return 1;
            return 0;
        }
    }

这样就可以使用sort排序MyClass对象数组了,完整代码如下。

    class MyClass : IComparable
    {
        public int TheValue;

        public int CompareTo(object obj)
        {
            MyClass mc = (MyClass)obj;
            if (this.TheValue < mc.TheValue) return -1;
            if (this.TheValue > mc.TheValue) return 1;
            return 0;
        }
    }

    class Program
    {
        static void PrintOut(string s, MyClass[] mc)
        {
            Console.Write(s);
            foreach (var m in mc)
                Console.Write($"{m.TheValue} ");
            Console.WriteLine(" ");
        }

        static void Main()
        {
            var myInt = new[] { 20, 4, 16, 9, 2 };

            MyClass[] mcArr = new MyClass[5];
            for(int i = 0; i < 5; i++)
            {
                mcArr[i] = new MyClass();
                mcArr[i].TheValue = myInt[i];
            }
            PrintOut("Initial Order: ", mcArr);
            Array.Sort(mcArr);
            PrintOut("Sorted Order: ", mcArr);
        }
    }
    //输出:
    //Initial Order: 20 4 16 9 2
    //Sorted Order: 2 4 9 16 20

声明接口

    interface IMyInterface1{
        int DoStuff(int nVar1, long lVar2);
        double DoOtherStuff(string s, long x);
    }

实现接口

只有类和结构才能实现接口。如前文的Sort所示,要实现接口,类或结构必须:

接口是引用类型

接口是一个引用类型,我们可以通过类对象引用强制转换为接口类型来获取指向接口的引用。一旦有了引用,就可以使用点语法来调用接口的成员。

IIfc ifc = (IIfc1)mc;//获取接口的引用
ifc.PrintOut("interface");//使用接口的引用调用方法

as运算符

ILiveBirth b = a as IliveBirth;//跟cast:(ILiveBirth)a一样
if(b!=null)
Console.WriteLine($"Baby is called:{b.BabyCalled()}");

访问显式接口成员实现

需要强制转换当前对象的引用(this)为接口类型的引用,并使用这个指向接口的引用来调用显式接口实现。

    class MyClass: IIfc1{
        void IIfc1.PringOut(string s){
            Console.WriteLine("IIfc1");
        }
        
        public void Method1(){
            PrintOut("...");//编译错误
            this.PringOut("...");//编译错误
            
            ((IIfc1)this).PrintOut("...");//正确调用
        }
    }