C#:单例,闭包,委托与事件,线程,Parallel,Params,扩展方法,接口与抽象类

.NET
281
0
0
2024-04-03
标签   C#

单例模式

在对泛型的约束中,最常使用的关键字有where 和 new。 其中where关键字是约束所使用的泛型,该泛型必须是where后面的类,或者继承自该类。 new()说明所使用的泛型,必须具有无参构造函数,这是为了能够正确的初始化对象

   /// <summary>
    /// C#单例模式
    /// </summary>
    public abstract class Singleton<T> where T : class,new()
    {
        private static T instance;
        private static object syncRoot = new Object();
        public static T Instance
        {
            get
            {
                if (instance == null)
                {
                    lock (syncRoot)
                    {
                        if (instance == null)
                            instance = new T();
                    }
                }
                return instance;
            }
        }

        protected Singleton()
        {
            Init();
        }

        public virtual void Init() { }
    }

1.泛型约束class Singleton where T : class,new() 2.静态对象没创建,使用new T() 3.在构造函数中可以加入虚方法 在上述示例中,我们使用泛型类型参数 T 来表示子类。where T : class, new() 约束了 T 必须是一个引用类型并且必须有一个无参构造函数。instance 变量和 Instance 属性与之前的示例相同。 当你需要扩展该单例类时,你只需创建一个继承自 Singleton 的子类,并在其中实现你的逻辑:

public class MySingleton : Singleton<MySingleton>
{
    // your code here
}

闭包陷阱

闭包是一个代码块(在C#中,指的是匿名方法或者Lambda表达式,也就是匿名函数),并且这个代码块使用到了代码块以外的变量,于是这个代码块和用到的代码块以外的变量(上下文)被“封闭地包在一起”。当使用此代码块时,该代码块里使用的外部变量的值,是使用该代码块时的值,并不一定是创建该代码块时的值。 一句话概括,闭包是一个包含了上下文环境的匿名函数。

动态给按钮回调传入参数,如果缺少int cur = i; 进入按钮的回调,按任何参数都是for循环i最后一个最大值

由于使用了 lambda 表达式作为 AddListener 的参数,变量 i 成为了被 lambda 表达式捕获的外部变量,所以变量 i 将不会被作为垃圾回收,直至引用变量的委托符合垃圾回收的条件。

i 的最终取值是 m_listContent.Count,这导致所有按钮都被使用lm_listContent.Count,和需求不符,解决方法是在每一轮循环中都定义新的变量,这样每一次 lambda 表达式都捕获了不同的变量,避免闭包陷阱。

for (int i = 0; i < m_listContent.Count; i++)
            {
                int cur = i;
                UIButton btn = m_listContent[i].GetComponent<UIButton>();
                
                btn.onClick.Add(new EventDelegate(delegate ()
                {
                    OnBtnGotoUrl(cur);
                    //错误写法OnBtnGotoUrl(i);
                }));
            }

委托与事件

① 委托把一个方法作为参数代入另外一个方法,理解为函数指针 ② 触发委托有2种方式: 委托实例.Invoke(参数列表),委托实例(参数列表) ③ 事件可以看作是一个委托类型的变量 ④ 通过+=为事件注册多个委托实例或多个方法 ⑤ 通过-=为事件注销多个委托实例或多个方法

delegate 是为了在C#中把函数作为对象传来传去而实现的一个“函数包装”,委托是具有相同签名的函数(方法)的类型。事件是委托的应用方式之,事件是一个属性/字段,类型是委托

delegate除了使用+=或-=来监听和移除方法,还可以用=,这样子使用会不小心把监听列表都覆盖掉的。 而event规范化了只能用+=和-=。

IDisposable

using

在 C# 中,using 语句是用于包裹一个实现 IDisposable 接口的对象的常见方式。IDisposable 接口提供了一种在使用完对象后释放资源的机制。 以下是一些常见的情况,在这些情况下你可以使用 using 语句来包裹对象: 1.文件操作:当你使用 FileStream、StreamReader、StreamWriter 等类进行文件读写时,通常会使用 using 来确保文件流在使用完后被正确关闭和释放资源。

using (FileStream fileStream = new FileStream("example.txt", FileMode.Open))
{
    // 使用文件流进行读写操作
}

2.数据库连接:当你使用 SqlConnection、SqlCommand、SqlDataReader 等类与数据库进行交互时,同样可以使用 using 来自动释放数据库连接和相关资源。

using (SqlConnection connection = new SqlConnection(connectionString))
{
    // 打开数据库连接并执行查询操作
}

3.网络请求:当你使用 HttpClient 或其他网络请求相关的类时,可以使用 using 来确保网络连接在使用完后被正确关闭。

using (HttpClient client = new HttpClient())
{
    // 发起网络请求
}

4.其他资源管理:任何实现了 IDisposable 接口的对象,如果需要在使用完后释放资源,都可以使用 using 语句来包裹。

using (SomeDisposableObject obj = new SomeDisposableObject())
{
    // 使用 obj 对象
}

使用 using 语句可以确保在代码块结束后,对象的 Dispose() 方法会被调用,从而释放资源。这样可以避免手动调用 Dispose() 方法或忘记释放资源的问题。

多次调Dispose

一个类型的Dispose方法应该允许被多次调用而不抛出异常。鉴于此,类型内部维护了一个私有的bool变量disposed,如下:

private bool m_Disposed;
/// <summary>
            /// 释放资源。
            /// </summary>
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            /// <summary>
            /// 释放资源。
            /// </summary>
            /// <param name="disposing">释放资源标记。</param>
            private void Dispose(bool disposing)
            {
                if (m_Disposed)
                {
                    return;
                }

                if (disposing)
                {
                    if (m_FileStream != null)
                    {
                        m_FileStream.Dispose();
                        m_FileStream = null;
                    }
                }

                m_Disposed = true;
            }

GC.SuppressFinalize(this); GC.SuppressFinalize 方法是用来通知垃圾回收器不要调用对象的析构函数(Finalize 方法)。它的作用是在对象已经被正确释放的情况下,避免不必要的资源回收操作,提高性能。 在 C# 中,当一个对象具有析构函数(Finalize 方法)时,垃圾回收器会在对象被垃圾回收之前调用该析构函数,以确保对象的资源得到正确释放。然而,在某些情况下,如果对象已经被显式地释放了,并且不再需要通过析构函数来释放资源,就可以使用 GC.SuppressFinalize 来通知垃圾回收器跳过对析构函数的调用。

不要创建过多线程

错误地创建过多线程的一个典型的例子是:为每一个Socket连接建立一个线程去管理。每个连接一个线程,意味着在32位系统的服务器不能同时管理超过约1000台的客户机。CLR为每个线程分配的内存会超过1MB。约1000个线程,加上.NET进程启动本身所占用的一些内存,即刻就耗尽了系统能分配给进程的最大可用地址空间2GB。即便应用程序在设计之初的需求设计书中说明,生产环境中客户端数目不会超过500台,在管理这500台客户端时进行线程上下文切换,也会损耗相当多的CPU时间。这类I/O密集型场合应该使用异步去完成

Parallel并行执行

在命名空间System.Threading.Tasks中,有一个静态类Parallel简化了在同步状态下的Task的操作。Parallel主要提供3个有用的方法:For、ForEach、Invoke。

static void Main(string[] args)  
{  
    int[] nums = new int[] { 1, 2, 3, 4 };  
    Parallel.For(0, nums.Length, (i) =>
    {  
        Console.WriteLine("针对数组索引{0}对应的那个元素{1}的一些工作代码……",i,  
            nums[i]);  
    });  
    Console.ReadKey();  
}

由于所有的任务都是并行的,所以它不保证先后次序。

Params传入参数

在 C# 中,使用 params 关键字作为函数参数传递不会直接导致垃圾回收(GC)。params 关键字所表示的参数数组是在编译期间就已经确定了大小并在运行时被创建的,不会引发额外的内存分配和释放操作。 当你调用带有 params 参数的函数时,编译器会将参数列表转换为一个数组,并将该数组传递给函数。这个数组在函数执行期间会存在于堆栈中,并在函数调用完成后被销毁。这个过程不会产生垃圾回收的开销。 然而,如果你在函数内部对 params 参数数组进行频繁的添加、插入、删除或修改等操作,这些操作可能会导致内存重新分配和释放,从而间接地增加垃圾回收的开销。因此,在设计代码时,应该尽量避免对 params 参数数组进行频繁的修改操作,或者考虑使用其他数据结构来替代 params 参数数组。 总的来说,params 参数本身不会直接产生垃圾回收,但如果在函数内部涉及到频繁的修改操作,可能会间接地增加垃圾回收的开销。因此,在设计和使用代码时,需要注意避免这些问题的出现。 还是有点难用,还是老实写多个函数重载吧

扩展方法

扩展方法除了让调用着可以像调用类型自身的方法一样去调用扩展方法外,它还有一些其他的主要优点: 可以扩展密封类型; 可以扩展第三方程序集中的类型; 扩展方法可以避免不必要的深度继承体系。 扩展方法还有一些必须遵循的要求: 扩展方法必须在静态类中,而且该类不能是一个嵌套类; 扩展方法必须是静态的; 扩展方法的第一个参数必须是要扩展的类型,而且必须加上this关键字; 不支持扩展属性、事件。 常见运用,C#中写设置Transform位置的扩展方法,给Lua调用,防止Lua传递Vector3造成性能消耗与类型转换

    public static void SetLocalPosition(this Transform transform, float x, float y, float z)
    {
        transform.localPosition = new Vector3(x, y, z);
    }

接口与抽象类

接口和抽象类有一些显而易见的区别: 1.接口支持多继承,抽象类则不能。 2.接口可以包含方法、属性、索引器、事件的签名,但不能有实现,抽象类则可以。 3.接口在增加新方法后,所有的继承者都必须重构,否则编译不通过,而抽象类则不需要。 这些区别导致两者的应用场景不同: 1.如果对象存在多个功能相近且关系紧密的版本,则使用抽象类。 2.如果关系不紧密,但若干功能拥有共同的声明,则使用接口。 3.抽象类适合于提供丰富功能的场合,接口则更倾向于提供单一的一组功能。 从某种角度来看,抽象类比接口更具备代码的重用性。子类无须编写代码即可具备一个共性的行为。 采用抽象类的另一个好处是,如果为为基类增加一个方法,则继承该基类的所有子类自然就会具备这个额外的方法,而接口却不能。如果接口增加一个方法,必须修改所有的子类。所以,接口一旦设计出来就应该是不变的。抽象类则可以随着版本的升级增加一些功能。 接口的作用更倾向于说明类型具有某个或者某种功能。接口只负责声明,而抽象基类往往还要负责实现。 接口的职责必须单一,在接口中的方法应该尽可能的简练。

用多态代替条件语句

    abstract class Commander
    {
        public abstract void Execute();
    }

class StartCommander : Commander
    {

        public override void Execute()
        {
            //启动
        }
    }

    class StopCommander : Commander
    {

        public override void Execute()
        {
            //停止
        }
    }

static void Main(string[] args)
        {
            Commander commander = new StartCommander();
            Drive(commander);
            commander = new StopCommander();
            Drive(commander);
        }

        static void Drive(Commander commander)
        {
            commander.Execute();
        }

将类型标识为sealed

sealed能够阻止类型被其他类型继承

使用事件访问器替换公开的事件成员变量

public class MyClass 
{ 
    // 声明事件
    private event EventHandler myEvent; 

    // 定义事件访问器
    public event EventHandler MyEvent 
    { 
        add { myEvent += value; } 
        remove { myEvent -= value; } 
    }

    // 触发事件的方法
    protected virtual void OnMyEvent() 
    { 
        EventHandler handler = myEvent; 
        if (handler != null) 
        { 
            handler(this, EventArgs.Empty); 
        } 
    } 
}

在上面的示例中,我们首先声明了一个私有的事件成员变量 myEvent,然后定义了一个公开的事件访问器 MyEvent。通过这个事件访问器,我们可以将事件添加到或从事件列表中删除事件。 在类中,使用 OnMyEvent() 方法来触发事件。该方法首先检查事件处理程序是否为空,如果不为空,则触发事件。