C#学习笔记

前排提要:

IComparable和IComparer:C#笔记25:比较和排序(IComparable和IComparer以及它们的泛型实现) - 陆敏技 - 博客园

Marshal:提供了一个方法集,这些方法用于分配非托管内存、复制非托管内存块、将托管类型转换为非托管类型,此外还提供了在与非托管代码交互时使用的其他杂项方法。详见:https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal?view=net-5.0

迭代器及yield关键字:C#中的IEnumerator 和 yield_u012138730的专栏-CSDN博客_ienumerator

C#实现Unity的协程:用C# 模拟实现unity里的协程 - 吃斤欢乐豆 - 博客园

C#代码托管执行过程:托管执行过程 | Microsoft Docs

C#委托的一点原理:深入理解C#委托及原理 - 记性特差 - 博客园

1e6大小的int数组大概4MB

C/C++和C#对比:C++和C#对比_TQT的博客-CSDN博客

C#中?的使用:C#中?的相关使用 - xyyh - 博客园

GC.SuppressFinalize:https://www.cnblogs.com/huangxincheng/p/12811291.html,简单来说就是通知CLR开启的线程不要进行当前对象托管资源和非托管资源的清理,而由我们手动执行。

这里再说下为什么要自定义析构函数:之所以这样做是因为GC只能进行托管资源清理,而无法清理非托管资源,这种情况的解决方法便是为当前类定义一个析构函数,负责释放非托管资源,当对象被清理时该函数会被CLR所开启的一个线程自动执行,这样,非托管资源和托管资源就都被清理了(托管资源的清理由GC自动进行)

析构函数(自动执行,在C#中常用来释放非托管资源):C#析构函数_天马3798-CSDN博客_c# 析构函数

IDisposable接口(该接口的Dispose方法不会自动执行):

如果您的应用程序在使用昂贵的外部资源,则还建议您提供一种在垃圾回收器释放对象前显式地释放资源的方式。可通过实现来自 IDisposable 接口的 Dispose 方法来完成这一点,该方法为对象执行必要的清理。这样可大大提高应用程序的性能。即使有这种对资源的显式控制,析构函数也是一种保护措施,可用来在对 Dispose 方法的调用失败时清理资源

详见:C#IDisposable 接口&资源释放_天马3798-CSDN博客

IDisposable接口和析构函数常一起使用,模板:

public class Foo : IDisposable
{
    private bool disposed = false;
    
    public void Dispose()
    {
        //既释放托管资源,又释放非托管资源
        Dispose(true);
        //将对象从垃圾回收器链表中移除,      
        //从而在垃圾回收器工作时,不进行托管和非托管资源的清理及内存的回收
        GC.SuppressFinalize(this);
    }
    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                //释放托管资源
            }
            // 释放非托管资源
            disposed = true;
        }
    }
    //析构函数不是人工调用,由垃圾回收器调用,常用于释放非托管资源
    ~Foo()
    {
        Dispose(false);
    }
}

using关键字:当对象继承IDisposable接口后在其它类中可以将该类对象的初始化放在using语句中,当using块中的内容执行完毕后会自动执行using语句中对象的Dispose方法:C# using 关键字使用整理_天马3798-CSDN博客

==与Equal及HashCode:Equals和==及GetHashCode简述_TQT的博客-CSDN博客

属性:C#给自动属性设置默认值_weixin_33937499的博客-CSDN博客

DateTime:C#中DateTime相关_TQT的博客-CSDN博客

枚举向其它类型的转换:C#枚举类型向其它类型的转换_TQT的博客-CSDN博客

C#预处理命令:C# 预处理指令详讲 - Robin99 - 博客园

C#中的内部类型:C#类中的内部类型_TQT的博客-CSDN博客

C#中的静态类:静态类和非静态类的主要差别 - yxwkaifa - 博客园

C#匿名类:https://www.cnblogs.com/myfqm/articles/12986480.htm

C#中的Internal关键字小结及解决方案、项目(程序集)、命名空间解释:https://www.jb51.net/article/112789.htm

C#in、ref、out关键字:C# 中 in,out,ref 的作用与区别 - Endv - 博客园

这里补充一下:out传递的参数在方法里必须要赋一次值,以及在调用out参数的函数时可以直接在调用的括号里作声明,如Test(out int a),非out未赋值的局部变量做参数时会报错

而in参数在调用函数传递参数时可以不用加in关键字,且该变量在方法是只读的

C#默认访问修饰符:C#默认访问修饰符_TQT的博客-CSDN博客

泛型约束:泛型中new()约束的用法_whaxrl的专栏-CSDN博客_变量类型没有new约束

序列化与反序列化:理解序列化和反序列化_TQT的博客-CSDN博客

字符串的内存分配:C#中字符串的内存分配与暂存池【非常详细】_xiaouncle的博客-CSDN博客

运算符重载:Operator运算符重载与Implicit隐式类型转换_张俊营-CSDN博客

类型转换:

括号强制类型转换和用as的区别:Unity -用括号强制类型转换与as区别_路漫漫其修远兮-CSDN博客

as和is的区别可以看C#本质论继承章

出自:int.parse和强制类型转换的区别 - 百度文库

主要语法:

byte与int相比占用内存更小但开销略大(CPU大多是32位或者64位,int更容易与之对齐)

一维数组:

int[] a = { 1, 3, 5 };
int[] b = new int[] { 1, 2, 3 };
int[] c = new int[3] { 1, 2, 3 };
//第一二种情况因为没有指定数组长度,因此赋值可以只写一个大括号,这样数组默认长度为0
//注意第三种情况,必须要写全三个值,不能省略(与C和C++不同)

二维数组:

//关于数组的声明:数组声明后一般都要创建,否则会报空指针
//关于数组的创建,无论以何种手段,只要能让编译器直到数组的长度,就不会报错,下面是详细的介绍
//关于数组的初始化,这个不是必要的,因为有默认值,特殊需求除外
using System;

namespace ConsoleApp1
{
    public class Program
    {
        static void Main(string[] args)
        {
            //矩形数组(第二维长度必须相等)
            //二维数组的声明
            int[,] a1;
            //二维数组声明并创建
            int[,] a2 = new int[2,3];
            //二维数组声明并初始化,注意这种赋值只能在声明时写,其它情况下要用new关键字
            //与下交错数组不同第二维不能用new关键字嵌套一维数组
            int[,] a3 = { { 1, 2, 3 }, { 4, 5, 6 } };
            //声明、创建、初始化也可以写在一块
            int[,] a4 = new int[2, 3] { { 1, 2, 3 }, { 4, 5, 6 } };
            //创建和初始化同时进行时创建可以不写数组的大小
            //注意与下面交错数组不同的是这里第一二维要么都写要么都不写
            int[,] a5 = new int[,] { { 1, 2, 3 }, { 4, 5, 6 } };

            //交错数组(第二维长度可以不相等)
            //声明
            int[][] b1;
            //声明并创建
            int[][] b2 = new int[2][];
            //声明并初始化,第二维必须用new关键字嵌套一维数组,可以把它理解为数组组成的数组,注意这种赋值只能在声明时写,其它情况下要用new关键字
            int[][] b3 = { new int[] { 1, 2, 3 }, new int[] { 4, 5 } };
            //声明、创建、初始化
            int[][] b4 = new int[2][] { new int[] { 1, 2, 3 }, new int[] { 4, 5 } };
            //创建和初始化同时进行时可以不写数组大小
            //注意交错数组第二维不能写
            int[][] b5 = new int[][] { new int[] { 1, 2, 3 }, new int[] { 4, 5 } };
        }
    }
}
//省略情况与一维数组相似,可以参考一维数组代码框相同位置处的注释

参数数组:

参数类型之前加上params关键字

具体的使用如下

using System;

namespace 参数数组
{
    class Program
    {
        static int Sum(params int[] array)
        {
            int ans = 0;
            foreach(int x in array)
                ans += x;
            return ans;

        }

        static void Main(string[] args)
        {
            Console.WriteLine(Sum(1,2,3,4,5,6,7,8,9));
        }
    }
}

泛型:

把类型作为参数,在初始化之前类型都是不确定的,类型的变量名一般用大写字母

//泛型类
using System;
using System.Collections.Generic;
using System.Text;

namespace 泛型
{
    class ClassA<T, A>
    {
        private T a;
        private T b;
        private A c;
        
        public ClassA(T a, T b)
        {
            this.a = a;
            this.b = b;
        }

        public string GetSum(A c)
        {
            return "" + a + b + c;
        }
    }
}

using System;

namespace 泛型
{
    class Program
    {
        //泛型方法
        static string MyGetSum<T, A>(T a, A b)
        {
            return ""+ a + b;
        }

        static void Main(string[] args)
        {
            //var x = new ClassA<int, double>(1, 2);
            //Console.WriteLine(x.GetSum(3.5));
            Console.WriteLine(MyGetSum<int, double>(2, 4.125));
        }
    }
}

利用泛型自写的一个列表:

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace 利用泛型和索引器实现列表
{
    class MyList<T> where T:IComparable//where关键词指定了T必须实现IComparable的接口才可以成功构造
    {
        T[] array = new T[1];
        int lastIndex = -1;

        public int Count
        {
            get
            {
                return lastIndex + 1;
            }
            
        }

        public void Add(T x)
        {
            int l = array.Length;
            if (lastIndex+1 == l)
            {
                T[] tmp = new T[l];
                tmp = array;
                array = new T[l * 2];
                for (int i = 0; i < l; i++)
                    array[i] = tmp[i];
            }
            lastIndex++;
            array[lastIndex] = x;
        }

        public void Insert(T x, int p)
        {
            if (p > lastIndex || p < 0) throw new Exception("索引取值错误");//抛出一个异常
            int l = array.Length;
            if (lastIndex + 1 == l)
            {
                T[] tmp = new T[l];
                tmp = array;
                array = new T[l * 2];
                for (int i = 0; i < l; i++)
                    array[i] = tmp[i];
            }
            for (int i = lastIndex; i >= p ; i--)
                array[i + 1] = array[i];
            array[p] = x;
            lastIndex++;
        }

        public T this[int index]//在set中可以用value代指自己要设置的值
        {
            get
            {
                if (index <= lastIndex && index >= 0) return array[index];
                else throw new Exception("索引取值错误");
            }
        }

        public void RemoveAt(int p)
        {
            if (p > lastIndex || p < 0) throw new Exception("索引取值错误");
            for (int i = p; i < lastIndex; i++)
                array[i] = array[i + 1];
            lastIndex--;
        }

        public int IndexOf(T x)
        {
            for (int i = 0; i <= lastIndex; i++)
                if (array[i].Equals(x)) return i;
            return -1;
        }

        public int LastIndexOf(T x)
        {
            for (int i = lastIndex; i >= 0; i--)
                if (array[i].Equals(x)) return i;
            return -1;
        }

        public void Sort()
        {
            for (int j = 1; j <= lastIndex-1; j++)
            {
                for (int i = 0; i <= lastIndex - j; i++)
                {
                    if (array[i].CompareTo(array[i + 1]) > 0)//泛型并不能直接比较,所以用CompareTo方法,大于0的话就是前面的后面的大,小于0,前面的比后面小,等于0相等,注意这个方法继承自IComparable接口
                    {
                        T tmp = array[i + 1];
                        array[i + 1] = array[i];
                        array[i] = tmp;
                    }
                }
            }
        }
    }
}

using System;

namespace 利用泛型和索引器实现列表
{
    class Program
    {
        static void Main(string[] args)
        {
            var myList = new MyList<int>();
            //Console.WriteLine(myList.Count);
            myList.Add(100);
            myList.Add(4);
            myList.Add(5);
            //Console.WriteLine(myList.Count);
            //myList.Insert(1, 2);
            //Console.WriteLine(myList.Count);
            //Console.WriteLine(myList[0]);
            //myList.RemoveAt(0);
            //Console.WriteLine(myList[0]);
            for (int i = 0; i < myList.Count; i++)
                Console.WriteLine(myList[i]);
            //Console.WriteLine(myList.IndexOf(5));
            myList.Sort();
            for (int i = 0; i < myList.Count; i++)
            {
                Console.WriteLine(myList[i]);
            }
        }
    }
}

构造函数:

using System;
using System.Collections.Generic;
using System.Text;

namespace 构造函数
{
    class Vector3
    {
        public double x, y, z;
        public Vector3()
        {
            x = 0;
            y = 0;
            z = 0;
        }
    }
}

using System;

namespace 构造函数
{
    class Program
    {
        static void Main(string[] args)
        {
            Vector3 force = new Vector3();//可重载,可传参
            Console.WriteLine(force.x + " " + force.y + " " + force.z);
        }
    }
}

函数重载:

using System;

namespace 函数重载
{
    class Program
    {
        static int MaxValue(params int[] array)
        {
            Console.WriteLine("int");
            int max = -1;
            foreach (int x in array)
                if (max < x) max = x;
            return max;
        }

        static double MaxValue(params double[] array)
        {
            Console.WriteLine("double");
            double max = -1;
            foreach (double x in array)
                if (max < x) max = x;
            return max;
        }
        //函数重载,最重要的是要相同函数名,参数不同,才可以视为重载

        static void Main(string[] args)
        {
            Console.WriteLine(MaxValue(1,2,3));
            Console.WriteLine(MaxValue(1.3, 2.2, 3.1));
        }
    }
}

接口:

using System;

namespace 接口
{
    //接口中的成员不需要加修饰符,都是公有的
    //一个接口可以继承另一个接口
    //一个类只能继承一个类而可以继承多个接口
    //接口的成员一般来说只有方法,属性,索引器,事件,不能有构造函数和字段,不允许运算符重载
    //接口中的任何成员不能提供实现方式 
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

using System;
using System.Collections.Generic;
using System.Text;

namespace 接口
{
    class Bird:IFly,IA,IB
    {
        public void Fly()
        {

        }

        public void MethodA()
        {

        }

        public void MethodB()
        {

        }

        public void FA()
        {

        }

        public void FB()
        {

        }
    }
}

using System;
using System.Collections.Generic;
using System.Text;

namespace 接口
{
    interface IFly
    {
        void Fly();
        void MethodA();
        void MethodB();

    }
}

using System;
using System.Collections.Generic;
using System.Text;

namespace 接口
{
    interface IA
    {
        void FA();
    }
}

using System;
using System.Collections.Generic;
using System.Text;

namespace 接口
{
    interface IB:IA
    {
        void FB();
    }
}

结构体函数:

using System;

namespace 结构体中的函数
{
    struct Name
    {
        public String firstName;
        public String lastName;
        public String GetName()
        {
            return firstName + " " + lastName; 
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Name myName;
            myName.firstName = "qt";
            myName.lastName = "t";
            Console.WriteLine(myName.GetName());
        }
    }
}

枚举类型:

using System;

namespace 枚举类型
{
    enum GameState:byte//本质默认为int
    {
        start = 100,//默认从0开始
        pause,//101
        during,//102
        end = 200
    }
    class Program
    {
        static void Main(string[] args)
        {
            GameState state1 = GameState.during;
            GameState state2 = GameState.end;
            Console.WriteLine(state1);
            Console.WriteLine((byte)state1);
            Console.WriteLine((byte)state2);
        }
    }
}

面向对象:

using System;

namespace 面向对象
{
    //在基类中声明的虚方法可以在派生类中进行重写
    //如果基类中某个方法不加virtual关键字,且派生类中写了相同的方法,那么派生类的对象在调用该方法时将会隐藏基类中的相应方法
    //base关键字可以调用基类中的方法
    //abstract关键字声明抽象类,抽象类中的方法分为普通方法和抽象方法,抽象方法同样用abstract关键字定义,抽象方法在派生类中必须重写,抽象类不能够被实例化
    //sealed作为类的关键字时表示该类为密封类,不能被继承,作为方法的关键字时表示方法为密封方法(只能在重写的方法上加该关键字),表示该方法不能被重写
    class Program
    {
        static void Main(string[] args)
        {
            Boss boss = new Boss();
            boss.Attack();
            boss.Move();
            Crow crow = new Crow();
            crow.Fly();
        }
    }
}

using System;
using System.Collections.Generic;
using System.Text;

namespace 面向对象
{
    class Enemy
    {
        public virtual void Attack()
        {
            Console.WriteLine("Enemy正在攻击");
        }

        public void Move()
        {
            Console.WriteLine("Enemy正在移动");
        }
    }
}

using System;
using System.Collections.Generic;
using System.Text;

namespace 面向对象
{
    class Boss:Enemy
    {
        public override void Attack()
        {
            base.Attack();
            Console.WriteLine("Boss正在攻击");
        }
    }
}

using System;
using System.Collections.Generic;
using System.Text;

namespace 面向对象
{
    abstract class Bird
    {
        public void Eat()
        {
            Console.WriteLine("鸟儿在捕食");
        }

        public abstract void Fly();
    }
}

using System;
using System.Collections.Generic;
using System.Text;

namespace 面向对象
{
    class Crow:Bird
    {
        public override void Fly()
        {
            Console.WriteLine("乌鸦正在飞");
        }
    }
}

派生类的构造函数:

using System;

namespace 派生类的构造函数
{
    class Program
    {
        static void Main(string[] args)
        {
            DerivedClass myDerivedClass1 = new DerivedClass();
            DerivedClass myDerivedClass2 = new DerivedClass(1, 2);
            //构造派生类时会默认先执行基类的构造函数,具体执行哪个构造函数在派生类中可以显式指定,如果没有指定则默认执行基类无参的构造函数
            //如果没有写任何构造函数,则类中会有一个默认的无参构造函数,如果写了,则没有
            //注意基类的构造函数在派生类中用到时要指定为非私有
        }
    }
}
 
namespace 派生类的构造函数
{
    class DerivedClass : BaseClass
    {
        private int y;
        public DerivedClass()
        {
            Console.WriteLine("这里是DerivedClass的无参构造函数");
        }

        public DerivedClass(int x, int y) : base(x)
        {
            Console.WriteLine("这里是DerivedClass的有参构造函数");
            this.y = y;
        }
    }
}
 
namespace 派生类的构造函数
{
    class BaseClass
    {
        private int x;
        public BaseClass()
        {
            Console.WriteLine("这里是BaseClass的无参构造函数");
        }

        public BaseClass(int x)
        {
            Console.WriteLine("这里是BaseClass的有参构造函数");
            this.x = x;
        }
    }
}

委托:

using System;

namespace 委托
{
    //委托变量也可以作为参数使用,但在作为参数使用前一定要初始化
    //Action声明的委托变量不能带返回值,加参数需要泛型,最多16个参数
    //Func声明的委托变量必须有一个返回值,加参数的话在返回值前面加(也就是返回值永远都在最右边),最多16个
    //多播委托若方法有返回值则只能得到最后一个方法的返回值
    //我们自定义的委托以及常用的内置委托都是派生于多播委托的
    //delegate匿名方法关键字,没有名字的方法
    delegate int Max(params int[] array);
    class Program
    {
        static int MaxValue(params int[] array)
        {
            int ans = -1;
            foreach (int x in array)
                if (ans < x) ans = x;
            return ans;
        }

        static void Test1()
        {
            Console.WriteLine("这里是Test1方法");
        }

        static void Test2(string str)
        {
            Console.WriteLine(str);
        }

        static int Test3()
        {
            return 1;
        }

        static int Test4(int x)
        {
            return x;
        }

        static void Test5()
        {
            Console.WriteLine("这里是Test5方法");
        }
        
        static void Test6()
        {
            Console.WriteLine("这里是Test6方法");
        }

        static void Main(string[] args)
        {
            //委托的基本使用(两种方式)
            //Max max = new Max(MaxValue);
            Max max = MaxValue;
            //Console.WriteLine(max(2, 5));
            Console.WriteLine(max.Invoke(2, 5));

            //Action委托的基本使用
            Action action1 = Test1;
            action1();
            Action<string> action2 = Test2;
            action2("这里是Test2的方法");

            //Func委托的基本使用
            Func<int> func1 = Test3;
            Console.WriteLine(func1());
            Func<int, int> func2 = Test4;
            Console.WriteLine(func2(2));

            //多播委托的基本使用
            Action action = Test6;
            action += Test5;
            //action -= Test6;
            //action.Invoke();
            Delegate[] delegates = action.GetInvocationList();//将多播委托转化为委托数组
            foreach(Delegate d in delegates)
                d.Invoke();

            //匿名方法
            Func<int, int, int> plus1 = delegate (int arg1, int arg2)
            {
                return arg1 + arg2;
            };

            //lambda表达式一般形式,参数不需要指定类型(在编译器无法推断参数类型时可以写,但如果是多个参数的话类型必须同时写或者同时不写)
            Func<int, int, int> plus2 = (arg1, arg2) =>
            {
                return arg1 + arg2;
            };
            Console.WriteLine(plus2(10, 20));
            //如果参数只有一个,那么不需要写括号,如果函数体只有一句,那么不用写大括号,这种情况下,如果方法有返回值的话编译器会自动加上return,没有的话则忽略
            Func<int, int> test = a => a + 1;
            Console.WriteLine(test(10));
        }
    }
}

关于lambda表达式的一个小坑:

using System;
using System.Collections.Generic;

namespace TestLambda
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Action> l = new List<Action>(); 

            for (int i = 0; i < 5; i++)
            {
                int index = i;
                l.Add(() => Log(i));
            }

            for (int i = 0; i < l.Count; i++)
            {
                l[i].Invoke();
            }

            Console.ReadKey();
        }

        private static void Log(int index)
        {
            Console.WriteLine(index);
        }
    }
}
//输出结果:
//5
//5
//5
//5
//5
//当把lambda表达式中的i换成index后输出正常
//0
//1
//2
//3
//4

原因:通过lambda表达式可以访问lambda表达式块外的变量(闭包)

上面的程序中在第二个for调用lambda表达式时外部变量i的值早在第一个for结束时就变为了5,所以输出全部为5。index的分析同理(原理见:理解C#中的闭包 - 黑洞视界 - 博客园

lambda表达式中的弃元:

从 C# 9.0 开始,可以使用弃元指定 lambda 表达式中不使用的两个或更多输入参数:

Func<int, int, int> constant = (_, _) => 42;

使用 lambda 表达式提供事件处理程序时,lambda 弃元参数可能很有用

ps:为了向后兼容,如果只有一个输入参数命名为 _,则在 lambda 表达式中,_ 将被视为该参数的名称

异常处理:

using System;

namespace 异常处理
{
    class Program
    {
        static void Main(string[] args)
        {
            //catch块可以有0或者多个,finall可以有0或者1个,如果没有catch块,必须有finally块,没有finally块,必须有catch块,catch和finally可以同时存在
            try//包含了可能出现异常的代码
            {
                int[] myArray = { 1, 2, 3, 4 };
                int myEle = myArray[4];
            }
            catch(IndexOutOfRangeException e)//如果这里不加参数,则任何异常都会捕捉,主要指明类型就行
            {
                //捕捉到相应异常后执行catch中的代码
                Console.WriteLine("发生了异常:IndexOutOfRangeException");
                Console.WriteLine("您访问数组的时候,下标越界了");
            }
            finally
            {
                //始终执行的代码,不管有没有异常都会执行
                Console.WriteLine("这里是finally里面的代码");
            }
            Console.WriteLine("Test");
        }
    }
}

一些细节:

1.当try、catch中有return时,finally中的代码依然会继续执行(exit退出进程可以跳过finally)

2.对于值类型的变量,当try、catch、finally中修改了某一个这样的变量并在try和catch中调用return返回该变量时,finally中的修改将不奏效;而对于引用类型且finally修改的是对象的成员(像是obj.member=xxx)而不是对象本身(obj = xxx)时,finally的修改将有效,修改对象本身则无效(以string为例,string类型是引用类型,对于string对象s本身的修改如s="new string"将无效,实际上,这句话就等于s=new string("new string"));原理跟值类型和引用类型的存储以及返回值的保存有关

3.finally中不允许调用return等离开finally块的方法,但可以抛异常,以及嵌套try-catch-finally

事件:

using System;

namespace 观察者设计模式_猫捉老鼠
{
    //事件和委托的区别是事件只能在类内调用,可以在类外订阅,也可以在类内订阅;委托无论调用还是订阅类内和类外都可以,但推荐不要在类外调用
    //事件和委托都只能在方法外定义,初始化可以在方法中
    class Program
    {
        
        static void Main(string[] args)
        {
            Cat cat = new Cat("加菲", "黄色");
            Mouse mouse1 = new Mouse("米奇", cat);
            Mouse mouse2 = new Mouse("杰瑞", cat);
            cat.CatComing();
        }
    }
}

using System;
using System.Collections.Generic;
using System.Text;

namespace 观察者设计模式_猫捉老鼠
{
    class Cat
    {
        private string name;
        private string color;

        public Cat(string name, string color)
        {
            this.name = name;
            this.color = color;
        }

        public void CatComing()
        {
            Console.WriteLine(color+"的"+name+"来了");
            action();
        }

        public event Action action;
    }
}

using System;
using System.Collections.Generic;
using System.Text;

namespace 观察者设计模式_猫捉老鼠
{
    class Mouse
    {
        private string name;

        public Mouse(string name, Cat cat)
        {
            this.name = name;
            cat.action += RunAway;
        }

        private void RunAway()
        {
            Console.WriteLine(name + "逃跑了");
        }
    }
}

委托和事件的补充:

1.事件变量不能在类外调用,同时也不能在类外赋值

2.事件和委托变量不需要先赋值(=)再注册(+=),可以直接注册(+=)

Dictionary和扩展方法:

扩展方法详细:C# 中扩展方法应用 - 千浔 - 博客园

private Dictionary<UIPanelType, string> panelPathDict;
//string path;
//panelPathDict.TryGetValue(panelType, out path);
//下面是扩展方法修改的字典类,作用与上面是相同的
string path = panelPathDict.TryGet(panelType);//查找元素
panelPathDict.Add(tmp.panelType, tmp.path);//添加元素

//扩展类:
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;

//扩展方法
//关于扩展方法特别要注意this关键字
public static class DictionaryExtension
{
    public static Tvalue TryGet<Tkey, Tvalue>(this Dictionary<Tkey, Tvalue> dict, Tkey key)
    {
        Tvalue value;
        dict.TryGetValue(key, out value);
        return value;
    }
}

序列化回调接口:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

[Serializable]
public class UIPanelInfo:ISerializationCallbackReceiver
{
    [NonSerialized]
    public UIPanelType panelType;
    public string panelTypeString;
    public string path;

    public void OnAfterDeserialize()//在反序列化后调用,反序列化即由文本到对象的过程
    {
        UIPanelType type = (UIPanelType)System.Enum.Parse(typeof(UIPanelType), panelTypeString);
        panelType = type;
    }

    public void OnBeforeSerialize()//在序列化之前调用,序列化即由对象到文本的过程
    {
        throw new NotImplementedException();
    }
}

StringBuilder:

using System;
using System.Text;//命名空间

//推荐当字符串内容改变频繁时使用
namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            //三种初始化
            //StringBuilder sb = new StringBuilder("xxx");
            //StringBuilder sb = new StringBuilder(20);
            StringBuilder sb = new StringBuilder("xxx", 20);
            //同string相比其内容可变,当字符串长度超过指定长度时会自动将容量扩大为当前的二倍
            //内容可变案例如下:
            sb.Append("/xxx");
            string s = "xxx";
            s = s + "/xxx";
            //前者没有引用的更换,直接将后面的字符串放到了前面的字符串的后面
            //后者是将当前引用的字符串和后面的字符串进行了拼接,之后存到新的地址单元,并将引用指向该地址单元

            //StringBuilder更多方法
            sb.Insert(0, "http://");//将待插入字符串插入到0下标及以后位置
            sb.Remove(0, 7);//将从0下标开始的7个字符移除
            sb.Replace("/", ",");sb.Replace(',', '/');//两种替换
            Console.WriteLine(sb);
        }
    }
}

string:

//基本
using System;

namespace StringStudy
{
    class Program
    {
        static void Main(string[] args)
        {
            // 声明但不初始化 
            string msg1;
            //声明并创建初始化为空
            string msg2 = new string("");
            // 声明并初始化为null 
            string msg3 = null;
            // 用String.Empty初始化
            string msg4 = String.Empty;
            // 取消转义1
            string oldPath = "c:\\windows";
            // 取消转义2 
            string newPath = @"c:\windows";
            // 使用const防止msg4被篡改
            const string msg5 = "I'm const!";
            // 可以使用匿名类型var
            var msg6 = "Hi!";
            // 使用字符数组初始化
            char[] letters = { 'A', 'B', 'C' };
            string alphabet = new String(letters);
            Console.Read();
        }
    }
}

//转义相关
//@可以取消转义
//\将其后的字符转义
using System;

namespace StringStudy
{
    class Program
    {
        static void Main(string[] args)
        {
            string str1 = "\"I am a good boy.But she is a bad girl!\"";//将"转义,可以输出双引号
            string str2 = @"I am a good boy.
But she is a bad girl!";
            string path1 = @"C:\swsetup\sp98700";
            string path2 = "C:\\swsetup\\sp98700";//将\转义,可以输出
            Console.WriteLine(path2);
            string str = Console.ReadLine();//输入
            Console.WriteLine(str);
            Console.ReadKey();//等待按键返回
        }
    }
}

//字符串方法
using System;

namespace StringStudy
{
    class Program
    {
        static void Main(string[] args)
        {
            //以下方法都不改变原有字符串

            //字符串比较方法
            //string s = "tqt";
            //int res = s.CompareTo("a");
            相等时返回0,s比较大(Ascii码)时返回1,否则返回-1
            //Console.WriteLine(res);

            不改变原字符串替换原字符串中的字符,也可以替换字符串
            //string s = "xxx";
            //string newS = s.Replace('x', 'a');
            //Console.WriteLine(s);
            //Console.WriteLine(newS);

            按照某个给定字符分割字符串,产生一个字符串数组
            //string s = "a-dd";
            //string[] newS = s.Split('-');
            //foreach (var tmp in newS)
            //    Console.WriteLine(tmp);

            //截取字符串,第一个参数为下标,第二个参数为截几个,没有第二个参数的话则默认到结尾
            //string s = "asd";
            //string newS = s.Substring(1, 2);
            //Console.WriteLine(newS);

            //ToLower()把字符串转换为小写形式,ToUpper()把字符串转换为大写形式

            截取字符串首尾空白
            //string s = "   sd";
            //string newS = s.Trim();
            //Console.WriteLine(newS);

            //查找子字符串并返回子字符串首字母的下标,若不存在,则返回-1
            string s = "123456";
            Console.WriteLine(s.IndexOf("123"));

            string.Format("广播事件错误:事件{0}对应委托具有不同的类型", eventType);//可以将后面的值带入按照顺序带入前面的字符串{}处,返回组合好的字符串
        }
    }
}

C++和C#String是否是'\0'结尾:

C++中没有规定string以'\0'结尾,但大部分编译器在实现时会为string结尾增加'\0'

C#string并不以'\0'结尾

C++同C一样,char数组最后一个必须是空字符,而C#不需要

文件操作:

//文件及文件夹信息
using System;
using System.IO;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            //取得路径下的文件信息,可以写相对路径(相对于输出目录的路径,一般情况下都是相对于C:\Users\tqt15\Desktop\ConsoleApp1\ConsoleApp1\bin\Debug\netcoreapp3.1该路径),也可以写绝对路径(完整的路径)
            //FileInfo fileInfo = new FileInfo("TextFile1.txt");
            //Console.WriteLine(fileInfo.Exists);//Exists只是其中的一个属性,其它属性或者方法可以自己测试
            //如:
            //fileInfo.Delete();//删除当前路径下的文件
            //fileInfo.CopyTo("FileName.txt");//将该文件复制一份在当前目录,且可以指定名字
            FileInfo fileInfo2 = new FileInfo("tqt.txt");
            if (!fileInfo2.Exists) fileInfo2.Create();//创建文件
            fileInfo2.MoveTo("tqt2.txt");//移动文件,也可以用来重命名

            DirectoryInfo dirInfo = new DirectoryInfo(@"C:\Users\tqt15\Desktop\ConsoleApp1\ConsoleApp1\bin\Debug");//获得文件夹信息,属性可按照上面的方法查看
        }   
    }
}
//使用File读写文件,一次性全部读写完成
using System;
using System.IO;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            //string[] lines = File.ReadAllLines("TextFile1.txt");//将目标文件中的每一行作为一个字符串,最后返回一个字符串数组
            //foreach(var line in lines)
            //{
            //    Console.WriteLine(line);
            //}
            //string s = File.ReadAllText("TextFile1.txt");//将所有字符(包括换行符空格等)返回为一个字符串
            //Console.WriteLine(s);
            //File.ReadAllBytes("Path");//非文本文件(如图片等)可以用这种方式来读,返回值为字节数组

            //File.WriteAllText("TextFile2.txt", "hello\n中国");//如果当前路径下文件存在则覆盖,否则则创建一个新的文件并写入  
            File.WriteAllLines("TextFile3.txt", new String[] { "asd", "ad" });//按行写入字符串数组
            //File.WriteAllBytes()//用法同上,主要用于非文本文件,参数为字节数组
        }
    }
}
//使用FileStream读写文件(主要用于非文本文件,操作单位为字节)
//分批次读写
using Microsoft.VisualBasic.FileIO;
using System;
using System.Diagnostics;
using System.IO;

namespace ConsoleApp3
{
    class Program
    {
        static void Main(string[] args)
        {
            //FileStream readStream = new FileStream("TextFile1.txt", FileMode.Open);
            //FileMode:
            //Append:如果存在文件,则打开文件并查找到文件尾,否则创建文件
            //Create:创建新文件,如果文件已存在,将被覆盖
            //CreateNew:创建新文件,如果文件存在,则会报异常
            //Open:打开现有的文件,若文件不存在,则报异常
            //打开文件,若文件不存在则创建

            //byte[] data = new byte[1024];
            //while (true)
            //{
            //    int length = readStream.Read(data, 0, data.Length);//读取流文件的数据,参数1为读取结果存储的数组,参数2表示读取的偏移,参数3表示一次最大读取的长度
            //    if (length == 0)//当一次读不完时需要多次读取,当有效读取长度为0时读取结束
            //    {
            //        Console.WriteLine("读取结束");
            //        break;
            //    }
            //    for (int i = 0; i < length; i++)
            //        Console.WriteLine(data[i]);
            //}

            //readStream.Close();//使用完流后要记得关闭

            //实例:使用流进行图片的复制
            FileStream readStream = new FileStream("源图片.png", FileMode.Open);
            FileStream writeStream = new FileStream("副本.png", FileMode.Create);

            byte[] data = new byte[1024];

            while (true)
            {
                int length = readStream.Read(data, 0, data.Length);
                if (length == 0)
                {
                    Console.WriteLine("读取结束");
                    break;
                }
                else writeStream.Write(data, 0, length);
            }
            writeStream.Close();
            readStream.Close();
        }
    }
}
//使用StreamReader和StreamWriter读写文件(文本文件,操作单位为字符)
//分批次读写
using System;
using System.IO;

namespace ConsoleApp4
{
    class Program
    {
        static void Main(string[] args)
        {
            StreamReader reader = new StreamReader("TextFile1.txt");

            //while (true)
            //{
            //    string str = reader.ReadLine();//读取一行字符串
            //    if (str == null) break;
            //    Console.WriteLine(str);
            //}

            //while (true)
            //{
            //    int res = reader.Read();//读取字符,包括换行符空格等
            //    if (res == -1) break;
            //    else Console.Write((char)res);
            //}

            //reader.Close();

            StreamWriter writer = new StreamWriter("TextFile2.txt");//若没有文件则创建,若有文件则覆盖

            while (true)
            {
                string message = Console.ReadLine();
                if (message == "q") break;
                //writer.Write(message);//写入字符串不换行
                writer.WriteLine(message);//写入字符串换行
            }
            writer.Close();
        }
    }
}

MemoryStream:C#中MemoryStream类的介绍_pan_junbiao的博客-CSDN博客_memorystream

反射(Type):

//基本使用
using System;
using System.Reflection;

namespace 反射
{
    class Program
    {
        static void Main(string[] args)
        {
            //每一个类对应一个type对象,这个type对象存储了这个类的信息
            MyClass my = new MyClass(); //一个类中的数据是存储在对象中的,但是type对象只存储类的成员
            Type type = my.GetType(); //通过对象获取这个对象所属类的Type对象
            Console.WriteLine(type.Name); //获取类的名字
            Console.WriteLine(type.Namespace); //获取所在的命名空间
            Console.WriteLine(type.Assembly); //获取所在程序集
            FieldInfo[] array1 = type.GetFields(); //只能获取public字段
            foreach (FieldInfo info in array1)
            {
                Console.WriteLine(info.Name);
            }
            PropertyInfo[] array2 = type.GetProperties(); //获取public属性
            foreach (PropertyInfo info in array2)
            {
                Console.WriteLine(info.Name);
            }
            MethodInfo[] array3 = type.GetMethods(); //获取public方法
            foreach (MethodInfo info in array3)
            {
                Console.WriteLine(info.Name);
            }
        }
    }
}

using System;
using System.Collections.Generic;
using System.Text;

namespace 反射
{
    class MyClass
    {
        private int id;
        private int age;
        public int number;
        public string Name { get; set; }

        public void Test1()
        {

        }
        public void Test2()
        {

        }
    }
}
//应用实例(单例模板)
using System;
using System.Reflection;

namespace RFramework
{
    //where T : Singleton<T> 表示对泛型T的限制,即T必须是继承自Singleton<T>的类型
    public abstract class Singleton<T> where T : Singleton<T>
    {
        protected static T mInstance = null;

        protected Singleton()
        {

        }

        public static T Instance
        {
            get
            {
                if (mInstance == null)
                {
                    //先获取所有非静态类的非public的构造方法
                    var ctors = typeof(T).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
                    //从ctors中获取无参的构造方法
                    var ctor = Array.Find(ctors, c => c.GetParameters().Length == 0);

                    if (ctor == null)
                        throw new Exception("Non-public ctor() not found!");
                    //调用构造方法,构造函数无返回值,默认返回构造的类型
                    mInstance = ctor.Invoke(null) as T;
                }

                return mInstance;
            }
        }
    }
}

Linq:

using System;
using System.Linq;
using System.Collections.Generic;

namespace ConsoleApp1
{
    class Program
    {
        private class Student
        {
            public int id;
            public string name;

            public Student(int id, string name)
            {
                this.id = id;
                this.name = name;
            }

            public override string ToString()
            {
                return "id:" + id + "\n" + "name:" + name;
            }
        }
        private class ScoreInfo
        {
            public int id;
            public int score;

            public ScoreInfo(int id, int score)
            {
                this.id = id;
                this.score = score;
            }

            public override string ToString()
            {
                return "id:" + id + "\n" + "score:" + score;
            }
        }
        static void Main(string[] args)
        {
            List<Student> students = new List<Student>
            {
                new Student(1, "A"),
                new Student(2, "B"),
                new Student(3, "C"),
                new Student(4, "D"),
                new Student(5, "E")
            };

            List<ScoreInfo> scoreList = new List<ScoreInfo>
            {
                new ScoreInfo(1, 10),
                new ScoreInfo(2, 8),
                new ScoreInfo(3, 9),
                new ScoreInfo(4, 7),
                new ScoreInfo(5, 5)
            };

            1.普通查询:
            from后面设置查询的集合
            //var res = from s in scoreList
            //              //where后面加查询条件
            //          where s.score > 7
            //          //select设置返回的形式
            //          select s.id;

            2.扩展方法查询:
            //var res = scoreList.Where(s => s.score > 7 && s.id <= 3).Select(s => s.id);

            3.联合查询
            在两个集合中查询,没有查询条件的话则会将两个集合中的元素组合在一起,如下面的例子
            没有where的话会输出students.length* people.length个结果,也就是将students中的每个元素和people中的每个元素依次组合
            包括所有可能
            //var res = from student in students
            //          from scoreInfo in scoreList
            //          where student.id == scoreInfo.id
            //          //匿名类,编译器会自动为我们生成一个类,该类的ToString方法也被重写,如下的例子
            //          //输出单个该匿名对象时,为{ id = 1, name = A, score = 10 }这种格式
            //          select new { id = student.id, name = student.name, score = scoreInfo.score };

            4.扩展方法写法
            SelectMany第一个参数为一个lambda表达式,该表达式的参数一般用不到,主要用该表达式的返回值来表示待联合的集合,第二个参数表示返回结果的形式
            这里同样使用了匿名类将两个集合的元素合并为一个元素
            Where和Select同上,只不过这里操作的单位是我们匿名类的对象
            //var res = students.SelectMany(scoreInfo => scoreList, (student, scoreInfo) => new { student = student, scoreInfo = scoreInfo })
            //    .Where(s => s.student.id == s.scoreInfo.id)
            //    .Select(s => new { id = s.student.id, name = s.student.name, score = s.scoreInfo.score });

            5.排序
            //var res = from scoreInfo in scoreList
            //          where scoreInfo.id >= 3
            //          //将score作为第一关键字(降序),id作为第二关键字(升序)排序
            //          orderby scoreInfo.score descending, scoreInfo.id
            //          select scoreInfo;

            6.扩展方法写法
            //var res = scoreList.Where(scoreInfo => scoreInfo.id >= 3)
            //    .OrderByDescending(scoreInfo => scoreInfo.score)
            //    .OrderBy(scoreInfo => scoreInfo.id);

            7.Join on 联合查询
            //var res = from student in students
            //              //join后面加待联合查询的另一个集合元素的泛称(起什么名都行),in后面加集合,on后面表示对联合的约束条件
            //              //其后的关键字大多数情况下都是equals
            //          join scoreInfo in scoreList on student.id equals scoreInfo.id
            //          //如果两个集合有一对多关系(一对一也可)的话可以在此处加一个into groups,会以少的一方为分类标准对多的一方进行分类,参考:https://blog.csdn.net/lym940928/article/details/80306460?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.control
            //          //可以用Where加其它约束条件
            //          select new { id = student.id, name = student.name, score = scoreInfo.score };

            8.按照自身字段分组
            //var res = from student in students
            //              //通过student的id分组,将结果放入groups中
            //              //key表示分组的依据,count表示该组数量
            //          group student by student.id
            //          into groups
            //          select new { id = groups.Key, count = groups.Count() };

            //foreach (var tmp in res)
            //{
            //    Console.WriteLine(tmp);
            //}

            //9.量词操作符
            //Any表示待查集合是否有学生名叫A
            //All表示带查集合所有学生是否都叫A
            Console.WriteLine(students.Any(student => student.name == "A"));
            Console.WriteLine(students.All(student => student.name == "A"));
        }
    }
}
//匿名类
//注意C#的匿名类与java的有很大不同
//详细:https://www.cnblogs.com/myfqm/articles/12986480.html
using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var tmpStudent = new
            {
                id = 1, //不能加类型
                name = "A"
            };
            Console.WriteLine(tmpStudent);
            //输出{ id = 1, name = A }
        }
    }
}

网络通信:

Socket基本:

//tcp服务器端
//需要建立连接,之后再进行收发数据
//程序退出时最好先关闭掉客户端的套接字,之后再关闭服务器端的套接字
//当前程序只提供对一个客户端的服务
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            //网络知识见博客推荐博客中的计算机网络博客推荐
            //建立一个套接字(所谓套接字即编程语言对网络通信中用到的一系列协议的封装)
            //第一个参数表示地址的种类,这里选择了4位ip地址
            //第二个参数表示传输过程中数据的形式,这里使用了数据流的形式
            //第三个参数表示用到的协议,这里使用了tcp协议
            Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //将服务器端的套接字与ip和端口号绑定
            IPAddress ipAddress = new IPAddress(new byte[] { 192, 168, 3, 23 });
            IPEndPoint point = new IPEndPoint(ipAddress, 7788);
            server.Bind(point);
            //开始监听(等待客户端连接)
            server.Listen(100);//最大连接数为100

            Socket client = server.Accept();//暂停当前线程,直到有一个客户端连接过来,之后执行下面的代码(注意只能是一个客户端)

            //使用返回的client套接字与客户端通信
            while(true)
            {
                string message = Console.ReadLine();
                byte[] data = Encoding.UTF8.GetBytes(message);
                client.Send(data);
            }
        }
    }
}

//tcp客户端
using System;
using System.Net.Sockets;
using System.Net;
using System.Text;

namespace Client1
{
    class Program
    {
        static void Main(string[] args)
        {
            //创建客户端的套接字
            Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            //用客户端的套接字连接服务器端
            IPAddress ipAddress = IPAddress.Parse("192.168.3.23");
            IPEndPoint point = new IPEndPoint(ipAddress, 7788);
            client.Connect(point);

            //接收数据
            while(true)
            {
                byte[] data = new byte[1024];
                int length = client.Receive(data); //停止线程,等待服务器端发送数据
                string message = Encoding.UTF8.GetString(data, 0, length);
                Console.WriteLine(message);
            } 
        }
    }
}

//udp服务器端
//不需要建立连接,直接进行数据的收发,方便的同时安全性也会降低
//没有明显的服务器端和客户端之分,谁先关闭套接字都可以
//可以对多个客户端服务
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace Server
{
    class Program
    {
        static Socket server;
        static void Main(string[] args)
        {
            //创建套接字,这里数据的形式是数据报的形式
            server = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

            //将服务器端的套接字与ip和端口号绑定
            server.Bind(new IPEndPoint(IPAddress.Parse("192.168.3.23"), 7788));

            //接收数据
            //注意不要将该线程设置为后台线程
            //应用程序必须运行完所有的前台线程才可以退出,而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束
            new Thread(ReceiveMessage).Start();
        }

        static void ReceiveMessage()
        {
            while(true)
            {
                EndPoint point = new IPEndPoint(IPAddress.Any, 0); //Any和0表示任意可用ip和端口号都可以与当前服务器建立连接
                byte[] data = new byte[1024];
                int length = server.ReceiveFrom(data, ref point); //暂停线程,等待数据
                string message = Encoding.UTF8.GetString(data, 0, length);
                Console.Write("从ip:" + (point as IPEndPoint).Address + ":" + (point as IPEndPoint).Port + "收到了数据:" + message);
            }
        }
    }
}

//udp客户端
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace Client1
{
    class Program
    {
        static void Main(string[] args)
        {
            //创建套接字
            Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            //发送数据
            while(true)
            {
                IPEndPoint point = new IPEndPoint(IPAddress.Parse("192.168.3.23"), 7788);
                string message = Console.ReadLine();
                byte[] data = Encoding.UTF8.GetBytes(message);
                client.SendTo(data, point);
            }
        }
    }
}

Socket用法补充(扩展):

//BeginConnect方法与Connect的方法的不同在于前者是另开一个线程进行异步连接操作的
IAsyncResult result = this.clientSocket.BeginConnect(this.address, null, null);
//IAsyncResult对象的AsyncWaitHandle.WaitOne方法:暂停当前线程一段时间,如果在这段时间内没有收到异步操作完成的信号,则返回false,否则返回true,可以用来判断异步连接是否成功
bool success = result.AsyncWaitHandle.WaitOne(NetConnectTimeout);

Socket对象的Poll()方法:
Poll 方法将会检查 Socket 的状态。指定 selectMode 参数的 SelectMode.SelectRead,可确定 Socket 是否为可读;指定 SelectMode.SelectWrite,可确定 Socket 是否为可写;使用 SelectMode.SelectError 检测是否有错误。Poll 将在指定的时段(以 microseconds 为单位)内等待响应结果,如果希望无限期的等待响应,则将 microSeconds 设置为一个负整数

SelectMode:

详细见:Socket.Poll()用法与说明_Just Do IT-CSDN博客_socket.poll

Socket的一些异步操作(这里只选了Accept、Receive、Send三种操作,其它操作可以类比):

AcceptAsync(参数)
ReceiveAsync(参数)
SendAsync(参数)
比起下面的方法更推荐上面的三个方法,参数是SocketAsyncEventArgs类型,通常使用时一般先定义一个方法注册SocketAsyncEventArgs对象的Completed事件,当某个异步操作完成时该方法会被调用,在方法中再次调用该异步操作即可(在调用之前可能需要对SocketAsyncEventArgs对象做一些调整,但可以循环使用)

BeginAccept
BeginReceive
BeginSend
EndAccept
EndReceive
EndSend
上面的三对方法用法直接看用例即可:

public void SendData(Byte[] data, Int32 offset, Int32 count)
{
    lock (this)
    {
        State state = eventArgs.UserToken as State;
        Socket socket = state.socket;
        if (socket.Connected)
            //指定回调方法,在回调方法中查看异步操作的结果        
            socket.BeginSend(data, 0, count, SocketFlags.None, new AsyncCallback(SendCallback), socket);
    }
}

private void SendCallback(IAsyncResult ar)
{
    try
    {
        // Retrieve the socket from the state object.
        Socket client = (Socket)ar.AsyncState;

        // Complete sending the data to the remote device.
        int bytesSent = client.EndSend(ar); //结束异步发送操作,返回值为成功发送的字节数
    }
    catch (Exception e)
    {
        Console.WriteLine(e.ToString());
    }
}

Socket的Blocking属性:

MSDN说明:Socket.Blocking Property (System.Net.Sockets) | Microsoft Docs

详细说明:https://blog.csdn.net/farrellcn/article/details/6394680?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control

官方手册:Socket 类 (System.Net.Sockets) | Microsoft Docs

TcpListener、TcpClient、UdpClient:

//TcpListener(服务器端)
using System;
using System.Net.Sockets;
using System.Net;
using System.Text;

namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            //TcpListener对Socket进行了一层封装,以快速创建套接字
            TcpListener listener = new TcpListener(IPAddress.Parse("192.168.3.23"), 7788);

            //开始监听
            listener.Start(100);

            //暂停当前线程,直到有一个客户端连接过来,之后执行后续代码(注意只能是一个客户端)
            TcpClient client = listener.AcceptTcpClient();

            //得到一个网络流,可以用这个流实现与客户端的通信
            NetworkStream stream = client.GetStream();

            //取得从客户端发送的数据
            byte[] data = new byte[1024];
            int length = stream.Read(data, 0, 1024);//注意这边会等待
            string message = Encoding.UTF8.GetString(data, 0, length);
            Console.WriteLine(message);

            stream.Close();
            client.Close();
            listener.Stop();
        }
    }
}

//TcpClient(客户端)
using System;
using System.Net.Sockets;
using System.Text;

namespace Client1
{
    class Program
    {
        static void Main(string[] args)
        {
            //创建TcpClient对象,并根据参数与服务器连接
            TcpClient client = new TcpClient("192.168.3.23", 7788);

            //得到与服务器通信的流
            NetworkStream stream = client.GetStream();

            //发送数据
            string message = Console.ReadLine();
            byte[] data = Encoding.UTF8.GetBytes(message);
            stream.Write(data, 0, data.Length);

            stream.Close();
            client.Close();
        }
    }
}

//UdpClient(服务器端)
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            //创建一个udpClient对象并绑定Ip和端口号
            UdpClient server = new UdpClient(new IPEndPoint(IPAddress.Parse("192.168.3.23"), 7788));

            //接收数据
            IPEndPoint point = new IPEndPoint(IPAddress.Any, 0);
            byte[] data = server.Receive(ref point); //通过point确定数据来自哪个ip的哪个端口,返回一个字节数组作为数据,等待
            string message = Encoding.UTF8.GetString(data);
            Console.WriteLine(message);

            server.Close();
        }
    }
}

//UdpClient(客户端)
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace Client1
{
    class Program
    {
        static void Main(string[] args)
        {
            //创建UdpClient对象
            UdpClient client = new UdpClient();

            //发送数据
            string message = Console.ReadLine();
            byte[] data = Encoding.UTF8.GetBytes(message);
            client.Send(data, data.Length, new IPEndPoint(IPAddress.Parse("192.168.3.23"), 7788));

            client.Close();
        }
    }
}

SocketAsyncEventArgs类常用方法(属性、事件):
说明见msdn:SocketAsyncEventArgs Class (System.Net.Sockets) | Microsoft Docs

建议配合上面的异步操作一起看

属性
AcceptSocket:保存当前服务器到连接到该服务器的客户端的套接字
Buffer:获取与异步操作一起使用的Buffer数组
BytesTransferred:获取异步操作中传输的字节数(未必是Buffer的长度,可能填不满)
Offset:字节为单位,Buffer的偏移
RemoteEndPoint:客户端的IP地址及端口号
SocketError:由于SocketAsyncEventArgs对象是用在异步的方法中的,异步方法执行完毕后往往需要调用Completed事件注册的方法,而在该方法中可以使用该属性查看连接的结果(是否成功)
UserToken:object类型,可以用来保存一些与当前异步操作有关的对象

方法
SetBuffer(参数):设置要与异步操作一起使用的Buffer数组
OnCompleted(参数):可以传一个lambda表达式用来表示异步操作完成时需要调用的方法,类似Completed
Dispose():在不需要该对象时调用,析构函数

事件
Completed:当异步操作完成时订阅该事件的方法将会被执行

线程:

//线程基本
using System;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            方法一:
            创建一个线程
            //Thread t = new Thread(DownLoad1);
            或者
            Thread t = new Thread(new ThreadStart(DownLoad1));
            启动线程
            //t.Start();
            //Console.WriteLine("Main");

            //方法二:
            //Thread t = new Thread(() =>
            //{
            //    Console.WriteLine("开始下载 线程ID:" + Thread.CurrentThread.ManagedThreadId);
            //    Thread.Sleep(2000);
            //    Console.WriteLine("下载完成");
            //});
            或者
            Thread t = new Thread(new ThreadStart(() =>
            {
                Console.WriteLine("开始下载 线程ID:" + Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(2000);
                Console.WriteLine("下载完成");
            }));
            //t.Start();
            //Console.WriteLine("Main");

            //方法三:
            Thread t = new Thread(DownLoad2);
            t.Start("Table1");
            Console.WriteLine("Main");
            

            //方法四:除了可以传静态方法外也可以传一个对象的非静态方法

            //线程对象的一些属性和方法:
            //ManagedThreadId,标识每个线程
            //IsBackground,该线程是否是后台线程
            //Priority,该线程的优先级,当两个线程的优先级不同时,低优先级的线程不会执行,只会执行高优先级的线程
            //如果存在多个优先级相同的线程,则每个线程都会执行
            //ThreadState,该线程的状态,线程状态可以百度
            //Abort(),终止调用该方法的线程对象的线程
            //Join(),将当前线程设置为WaitSleepJoin状态(当前线程Join之后的语句均不执行),直到调用该方法的线程对象执行完才继续执行当前线程(当前线程后续语句)
            //Sleep(),静态方法,使当前正在执行的线程休眠
        }

        //用于方法一
        static void DownLoad1()
        {
            //线程ID用于线程的管理
            Console.WriteLine("开始下载 线程ID:" + Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(2000);
            Console.WriteLine("下载完成");
        }

        //用于方法三
        static void DownLoad2(object fileName)
        {
            //线程ID用于线程的管理
            Console.WriteLine("开始下载:" + fileName + " 线程ID:" + Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(2000);
            Console.WriteLine("下载完成");
        }
    }
}

//线程池
//都是后台线程且不能修改
//不能修改线程优先级和名称
//只能用于时间较短的任务
using System;
using System.Threading;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadMethod));
            ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadMethod));
            ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadMethod));
            Console.ReadKey();
        }

        static void ThreadMethod(object obj)
        {
            Console.WriteLine("ID:"+Thread.CurrentThread.ManagedThreadId+"开始");
            Thread.Sleep(2000);
            Console.WriteLine("结束");
        }
    }
}

//线程加锁
using System;
using System.Threading;

namespace ConsoleApp4
{
    class Program
    {
        static void Main(string[] args)
        {
            MyThreadObject o = new MyThreadObject();
            Thread t = new Thread(ChangeNum);
            t.Start(o);
            new Thread(ChangeNum).Start(o);
            //两个线程同时访问一个对象,模拟冲突
        }
        static void ChangeNum(object para)
        {
            MyThreadObject o = para as MyThreadObject;
            while (true)
            {
                //当前线程执行到这里时会申请锁定o对象,
                //如果o对象没有被其他线程锁定,则当前线程锁定成功,
                //执行其中的语句,否则锁定不成功,此时当前线程会一直等待,
                //直到申请到该对象的锁定
                lock(o)
                {
                    o.ChangeNum();
                }//当前线程取消对o的锁定
            }
        }
    }
}

线程补充:上述方法创建的线程默认是前台线程,而SocketAsyncEventArgs对象的Completed事件所注册的方法则会默认在后台线程中执行,所有前台线程执行完之后不管后台线程是否执行完当前程序都会退出

任务:

//任务(与线程并不是简单封装的关系)
//任务的运行不会阻塞主线程,主线程结束后,任务一定也会结束
//线程无父子关系,但任务有父子关系,
//在一个任务中启动一个新的任务,则新任务是旧任务的儿子,
//两个任务异步执行,只有当所有子任务都执行完,父任务才执行完
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp3
{
    class Program
    {
        static void Main(string[] args)
        {
            任务基本:
            方法一:
            //Task t = new Task(ThreadMethod);
            //t.Start();

            方法二:
            //TaskFactory tf = new TaskFactory();
            //tf.StartNew(ThreadMethod);

            //Console.WriteLine("Main");

            //ContinueWith方法:
            Task t1 = new Task(DoFirst);
            //t1任务执行完后才执行t2和t3
            Task t2 = t1.ContinueWith(DoSecond);
            Task t3 = t1.ContinueWith(DoSecond);
            //t2执行完后才执行t4
            Task t4 = t2.ContinueWith(DoSecond);

            t1.Start();

            Console.ReadKey();
        }

        //任务基本用
        static void ThreadMethod()
        {
            Console.WriteLine("任务开始");
            Thread.Sleep(2000);
            Console.WriteLine("任务结束");
        }

        //ContinueWith方法用
        static void DoFirst()
        {
            Console.WriteLine("DoFirst:");
            Console.WriteLine("Task" + Task.CurrentId + "开始");
            Thread.Sleep(2000);
            Console.WriteLine("Task" + Task.CurrentId + "结束");
        }

        ContinueWith方法用
        static void DoSecond(Task t)
        {
            //这里的参数t表示上一个任务
            Console.WriteLine("DoSecond:");
            Console.WriteLine("Task" + Task.CurrentId + "开始");
            Thread.Sleep(2000);
            Console.WriteLine("Task" + Task.CurrentId + "结束");
        }
    }
}

琐碎知识点:

设计模式的基本思想:

对复杂的模块进行封装

对需求容易变动的模块提取封装为接口或者抽象类

方法修饰符:

父类中的protected对象,外界不能调用,只有自己及自己的子类可以继承和调用

父类中的private对象,外界不能调用,子类能够继承,但不能直接调用(可以通过父类的方法间接调用)

父类中的public对象,外界可以调用,子类能够继承和调用

internal和protected辨析:

internal(内部):限定的是只有在同一程序集中可访问,可以跨类

protected(受保护):限定的是只有在继承的子类中可访问,可以跨程序集

protected internal:受保护“或”内部修饰符修饰成员,当父类与子类在同一个程序集中,internal成员可见。当父类与子类不在同一个程序集中,子类不能访问父类internal成员,而子类可以访问父类的ptotected

internal成员,即,从当前程序集或从包含类派生的类型,可以访问具有访问修饰符protected internal的类型或成员

出自:https://www.jb51.net/article/112789.htm

C#中virtual关键字用来声明虚方法,可以在子类中对其重写,但不可改变方法类型

字段类型:

const 静态常量

参数数组(params):定义时将参数设置为参数数组,调用时可以直接传入多个值,而不需要将它们作成数组

宏:

#pragma warning disable 0618 //宏,在当前脚本中不报0618的警告

#if UNITY_STANDALONE_WIN 宏,判断当前系统是否为Windows系统

如果是的话,编译该区域的命令

#elif UNITY_ANDROID 宏,判断是否为Android系统

如果是的话,编译该区域的命令

#endif 宏结束的标志

#region XXX

其内的代码可以折叠

#endregion

关于继承:

内存原理:关于继承内存的分配和运行---用C#的方式去理解更加有效果_技术交流_牛客网

子类可以转父类,转为父类的子类还可以再转回来,但如果父类直接转为子类,则很多子类特有的字段可能为null

子类隐藏(相关方法前没有override关键字)和重写(相关方法前有override关键字)父类方法的区别:

子类重写方法后,对于子类的任何实例,父类的原方法a都已经不存在

子类隐藏父类方法后,父类的原方法仍然存在,当子类的实例的类型转为父类时,调用的方法a即为原来的方法

List<子类>不能上转型成为List<父类>
子类的上转型对象能够调用重写的方法操作子类中的字段(上转型对象调用被子类重写的父类方法还是按照重写的来),尽管它已经失去了该字段(java同理)

C#中抽象方法和虚方法的区别:

C#接口和抽象类的区别:

概念上:接口偏向于为其实现类增加某一功能而抽象类则更像是其派生类的一个模板,规定了派生类所应遵循的规范
具体使用上:
一个类可以实现多个接口但只能继承一个抽象类
实现抽象方法时必须加override关键字否则则认为是隐藏,实现接口方法时可以不加

补充:接口中的方法相当于抽象方法(没有方法体,继承类必须实现),但不是抽象方法(不需要加abstract关键字,加了也相当于没加,且接口方法不支持隐藏,使用隐藏的写法时会默认为重写,相应地,接口方法在重写时不用加override)

后端:

多线程访问同一资源时,如果每个线程只读,那么不需要加锁,如果其中有一个或多个线程可读可写其它线程只读,那么也需要加锁

对于一个线程写其它线程读的情况的补充:

单核情况下:根据写线程中所有对共享变量的操作是否需要原子化而定。如果不需要,就算是这个操作的中间值也可以,那么可以加锁也可以不加,如果需要原子化则必须加锁

多核情况下:即使是原子化的操作,也必须加锁,因为多核真的可能同时操作一块内存,不能保证数据的完整性

Interlocked.Increment,参数为一个ref的int或者long,对该数做原子递增操作,返回递增后的值,其它原子操作可以自行百度

其它:

Summary注释:

/// <summary>
/// Add
/// </summary>
/// <param name="para1">para1</param>
/// <param name="para2">para2</param>
/// <returns>para1+para2</returns>
public int Add(int para1, int para2)
{
    return para1 + para2;
}

一些编程细节(与java对比学习得来)

1.在一个文件中写多个类需要注意的事情:
可以在同一个文件中写多个类,无论类的访问权限是什么,编译后与写在多个文件中没有不同
main方法可以随意放(公有私有类都可以),可以有多个,由程序员选择执行哪个

2.在方法中声明并创建一个数组,此时数组虽然是局部变量,但默认的构造方法会使其值为0

3.对于一个3行5列的二维数组a,a.length的值为3,a[0]-a[4].length的值为5,注意第二维数组也可以不等长

4.科学计数法所表示的数都是double类型

5.高精度赋值给低精度,需要显式转换,否则会报错(如double给float)。低精度赋值给高精度不需要(int给float),系统自动转换

6.if语句括号中的数据必须是bool类型,其它与if类似的语句也是

7.switch可以没有default,但必须有break

8.算数混合运算的精度
精度由低到高(参考java,C#可以类比):
byte short char int long float double
一般来说一个表达式计算结果的类型会与该表达式中最高精度的数据类型相同
在表达式中最高精度低于int时,按照int来算

9.static方法不可以重载

10.类中的类方法只能操作类变量,不可以操作实例变量

11.用new运算符和构造方法创建对象时,步骤如下
为成员分配内存,并指定默认值
初始化成员变量,即用户声明成员变量时给定的默认值
执行构造方法
计算出一个引用值

12.
构造派生类时会默认先执行基类的构造函数,具体执行哪个构造函数在派生类中可以显式指定,如果没有指定则默认执行基类无参的构造函数
如果没有写任何构造函数,则类中会有一个默认的无参构造函数,如果写了,则没有
 注意基类的构造函数在派生类中用到时要指定为非私有

13.
接口中的方法默认public,在重写时方法前要加public,因为此时非接口类中方法默认为private

14.不能用sealed修饰构造方法

15.string和StringBuilder的区别:
string类型本质上是像数组一样的类型,即引用类型,但与数组不同的是其值不可改变,若使用诸如s+="s"的语句对string类型的变量s进行操作时,实际上s保存的引用会改变,改变后的引用指向地址的内容为s+"s",但原来的引用(改变前的引用)并没有变,其指向地址所保存的字符串也没有变
StringBuffer类型的变量其值可以修改(与数组类似),但需要使用Append函数

16.局部变量所谓的被释放掉,是指指向某个对象的引用从所执行线程的栈中POP出去了
但是那个对象实体还是存在的,只不过指向它的引用少了一个,如果不再有引用指向它,那么,GG才会处理它
(与C语言不同)

17.var只能用来声明局部变量

所有的ASCII码都可以用“\”加数字(一般是8进制数字)来表示。而C中定义了一些字母前加"\"来表示常见的那些不能显示的ASCII字符,如\0,\t,\n等,就称为转义字符,因为后面的字符,前面加上"\"之后都不是它本来的ASCII字符意思了

C#的成员变量可以在声明时直接进行赋值,也可以不赋值,此时会由无参的构造函数进行赋值。但局部变量必须赋值,否则编译器会警告

BitConverter.ToInt32(buf,0) 可以将byte[]转换成有符号的int型,其中参数buf为byte[]型,0为数组的起始位置

特别需要注意的是:buf中的字节从左到右的表示的是从低位到高位,与通常我们高位在左低位在右的方式不一样

[DllImport("kernel32.dll")]
static extern bool QueryPerformanceCounter([In, Out] ref long lpPerformanceCount);

[DllImport("kernel32.dll")]特性,即引入kernel32.dll文件中的QueryPerformanceCounter方法

至于[In,Out]可以理解为ref的另一种表达,这里有了ref也可以去掉

结构体反复通过new来创建,它的地址是不变的,都是在栈上,只不过相当于利用构造方法构造了调用的结构体变量

Console.Error是一个写入流,程序运行时出现的错误信息会被自动写入进去,同时也是一个标准输出流,被写入的错误信息会被自动显示在控制台界面

foreach遍历时
不能改变集合元素的值
不能增减元素
foreach为何不能改:这样设计的目的在于使正在被遍历的数据集合不被其它线程改变,但这样也使得我们不能在当前线程中修改正在遍历的数据集合
详细见
https://www.cnblogs.com/liyong888/p/7799272.html

字典中的键为List类型时,即使List对象中元素数目改变或者元素值改变,其所对应的键依旧不变

readonly和const和static:

readonly在声明时不需要赋值,运行中一旦赋值便不可以再次赋值(运行时常量),const声明时必须赋值,之后不可以赋值(编译时常量)

readonly不是静态的,const是静态

as转换不成功返回null

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
前 言 6 第1章 进程、线程与网络协议 7 1.1 进程和线程 7 1.1.1 Process类 7 1.1.2 Thread类 9 1.1.3 在一个线程中操作另一个线程的控件 13 1.2 IP地址与端口 15 1.2.1 TCP/IP网络协议 16 1.2.2 IPAddress类与Dns类 17 1.2.3 IPHostEntry类 17 1.2.4 IPEndPoint类 17 1.3 套接字 19 1.3.1 Socket类 20 1.3.2 面向连接的套接字 21 1.3.3 无连接的套接字 23 1.4 网络流 24 1.5 习题1 25 第2章 TCP应用编程 27 2.1 同步TCP应用编程 28 2.1.1 使用套接字发送和接收数据 28 2.1.2 使用NetworkStream对象发送和接收数据 30 2.1.3 TcpClient与TcpListener类 31 2.1.4 解决TCP协议的无消息边界问题 33 2.2 利用同步TCP编写网络游戏 34 2.2.1 服务器端编程 34 2.2.2 客户端编程 49 2.3 异步TCP应用编程 66 2.3.1 EventWaitHandle类 67 2.3.2 AsyncCallback委托 69 2.3.3 BeginAcceptTcpClient方法和EndAcceptTcpClient方法 70 2.3.4 BeginConnect方法和EndConnect方法 70 2.3.5 发送数据 71 2.3.6 接收数据 72 2.4 异步TCP聊天程序 73 2.4.1 服务器端设计 73 2.4.2 客户端设计 79 2.5 习题2 83 第3章 UDP应用编程 84 3.1 UDP协议基础知识 84 3.2 UDP应用编程技术 84 3.2.1 UdpClient类 84 3.2.2 发送和接收数据的方法 86 3.3 利用UDP协议进行广播和组播 90 3.3.1 通过Internet实现群发功能 90 3.3.2 在Internet上举行网络会议讨论 96 3.4 习题3 101 第4章 P2P应用编程 102 4.1 P2P基础知识 102 4.2 P2P应用举例 104 4.3 习题4 114 第5章 SMTP与POP3应用编程 115 5.1 通过应用程序发送电子邮件 115 5.1.1 SMTP协议 115 5.1.2 发送邮件 116 5.2 利用同步TCP接收电子邮件 120 5.2.1 POP3工作原理 121 5.2.2 邮件接收处理 123 5.3 习题5 127 第6章 网络数据加密与解密 128 6.1 对称加密 128 6.2 不对称加密 133 6.3 通过网络传递加密数据 136 6.4 Hash算法与数字签名 152 6.5 习题6 153 第7章 三维设计与多媒体编程 154 7.1 简单的3D设计入门 154 7.2 DirectX基础知识 160 7.2.1 左手坐标系与右手坐标系 160 7.2.2 设备 160 7.2.3 顶点与顶点缓冲 161 7.2.4 Mesh对象 162 7.2.5 法线 162 7.2.6 纹理与纹理映射 162 7.2.7 世界矩阵、投影矩阵与视图矩阵 162 7.2.8 背面剔除 164 7.3 Primitive 164 7.4 Mesh 171 7.5 灯光与材质 177 7.6 音频与视频 187 7.7 直接使用SoundPlayer类播放WAV音频文件 191 7.8 习题7 193 第8章 上机实验指导 194 8.1 实验一 简单网络聊天系统 194 8.2 实验二 网络呼叫应答提醒系统 195 8.3 实验三 文件数据加密与解密 199
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值