.net mvc 项目中 static class 和 singleton 有什么区别?

What is the difference between static class and singleton in .net mvc project?

我了解静态 class 和单例之间的区别,重要的是单例可以实例化一次,因为静态 class 不需要实例。

这个问题是从 .net mvc 项目的角度出发的,帮助我在使用两者之间做出决定。

所以假设我有 Class(es) 方法如下面给出的例子:

  1. 我有一个方法 ConvertMeterToMiles(int mtr),这里没有注入依赖。

  2. 或类似 SendEmail(str eaddress) 的方法,其中没有注入依赖项,但它实例化 new SMTPClient...,然后在 finally

    中处理 SMTPClient

假设我想将该方法放入实用程序服务 class,那么我应该创建静态 class 还是单例(具有依赖注入的资源)?

我知道作用域或瞬态没有意义,因为拥有新实例没有任何好处。

通过依赖注入添加单例会实例化请求的 class 的 实例 。静态 class 无法实例化,因此您只需根据静态 class 的访问修饰符从其他地方访问其方法。

静态class 一个单例。将单例创建为静态字段或通过依赖注入创建单例之间的唯一区别在于它的访问方式,仅此而已。

但在 SmtpClient 的上下文中,not thread-safe. In fact, the documentation 表示:

Note

If there is an email transmission in progress and you call SendAsync or Send again, you will receive an InvalidOperationException.

换句话说,您不能使用 SmtpClient 的同一个实例同时发送两封电子邮件。所以使用 SmtpClient 作为任何一种单例无论如何都不是一个好主意。你最好要么让它有作用域,要么根本不使用 DI,只在你需要的时候声明一个新的。

我想说的是,在您的应用程序上下文中,差异归结为是否使用 DI 以及谁在控制 lifetime/instantiation/injection。

将一些功能转移到一些静态助手 class 中可能会很好,如果它不应该根据环境或其他一些可变性来源而有所不同的话。例如 ConvertMeterToMiles 似乎是这种处理的一个很好的候选者。

SendEmail 另一方面似乎不是一个 - 可能存在您不想发送电子邮件的环境(例如测试),或者将来您预计会有多个实现(或需要重新实现它)以获得此功能(例如,对于某些上下文,可以使用队列延迟电子邮件发送,例如,它将由某些后台工作人员或其他服务处理)。在这种情况下,您可以高度利用 DI 的存在并封装此功能并将其隐藏在合同后面(另外我会说处理 SMTPClient 设置在 DI 中注册并解析为封装实现时更清晰)。

我建议使用注入的单例。从功能上讲,它几乎没有什么区别,但是注入单例在测试时有 很大的优势

虽然静态方法本身很容易测试,但测试独立使用它们的代码变得非常困难。

让我们以您的 SendEmail(str eaddress) 为例。如果我们将其实现为静态帮助程序方法,则在不创建真正的 SMTPClient 的情况下无法对使用此方法的代码进行单元测试。相比之下,如果我们注入一个带有接口的单例助手 class,则在测试调用 SendEmail 的代码时可以模拟该接口。

正如你所说的单例可以被实例化一次,所以它是保持对象存活的好地方,你可以在应用程序生命周期中使用的对象。对于 mvc 项目,单例对象对于每个请求都是相同的。

在您的方法 2 中,您的 SmtpClient 不需要每次都创建一个新实例并处理它。

来自 msdn 文档:

The SmtpClient class implementation pools SMTP connections so that it can avoid the overhead of re-establishing a connection for every message to the same server. An application may re-use the same SmtpClient object to send many different emails to the same SMTP server and to many different SMTP servers. As a result, there is no way to determine when an application is finished using the SmtpClient object and it should be cleaned up.

所以它是单例实用程序服务的一个很好的候选者。

单例模式如下所示:

public class SmtpUtilityService : ISmtpUtilityService, IDisposable
{
    private readonly SmtpClient _smtpClient;
    
    public SmtpUtilityService()
    {
        _smtpClient = new SmtpClient([...]);
    }
    
    public async Task SendEmail(str eaddress)
    {
        await _smtpClient.SendAsync([...]);
    }

    public void Dispose()
    {
        if(_smtpClient != null)
        {
            _smtpClient.Dispose();
        }
    }
}

在您的 Statup.cs 中,将 SmtpUtilityService 作为单例添加到 IServiceCollection 中,您的 SmtpClient 将仅实例化一次。

顺便说一句,微软不推荐使用 SmtpClient(在某些平台上已过时,不推荐在其他平台上使用)所以不确定它是否是一个好的候选者:/

msdn smtpclient obsolete

对于您的第一个方法 ConvertMeterToMiles(int mtr),它只是转换,一次一个计算。它不需要任何属性,也不需要实例。 所以全静态 class 是不错的选择。

public static class MeterHelper
{
    public static decimal ConvertMeterToMiles(int mtr)
    {
        return mtr * 0.0006213712;
    }
}

就我个人而言,我并不经常使用单例。如果我需要属性,我将使用作用域或瞬态服务,如果不需要,我将使用完整的静态 class(助手)。

Singleton 和 Static 在功能上没有区别。

在您描述的线程环境中,唯一会出现问题的是共享数据。如果多个线程访问相同的属性,将会出现并发问题。

现在,如果您只是使用实用方法,例如Sum(int a, int b),没有任何状态,就不会有任何问题。

现在,除了需要注入单例之外,这两种情况基本上没有区别。即使与网络有关api,也没有什么特别的。

除了单例 class 可以继承,而静态 class 不能。但那是另一个话题了。

让我先回顾一下:静态 class 不能有实例,因此您可以将其方法与 class 的名称一起使用,而单例 class 只能有 1 个实例被其他人分享。

对于静态 class,您需要将其包含在您的代码文件中。对于单例 class,您将首先创建一个实例,然后将其传递给参数列表中的其他方法。

在 aspnet 的上下文中,由于我们只有 1 个实例,我们将它的创建和处置留给框架 services.AddSingleton<ISingleton, Singleton>();,并通过 public SomeController(ISingleton singleton) 将其交给我们的控制器。当访问者点击这个控制器端点时,他们的所有请求都将不同,但随后由这个单一实例处理。 aspnet 将通过接口确定您的控制器需要哪些单例,并仅注入请求的单例。

无论您是服务器端全局状态持有者、数据库连接器还是电子邮件发件人,您实现的所有 activity 都将通过此单例实例。您可以在其中实施负载平衡器,以便可以无瓶颈地处理请求。

另一方面,对于静态 class,您会更喜欢短期方法,因为它们会 运行 分别针对每个对控制器的请求。距离转换器就是这样一种方法。它不需要任何冗长的流程来完成它的工作,也不会依赖于其他昂贵的资源。但是,您可能希望缓存最频繁的计算并从缓存中发送响应,然后将此距离转换器转换为长时间使用资源的单体将是一个更好的主意。

所以简而言之,根据资源的使用情况,您会更喜欢短期独立方法或具有大量昂贵操作的长期方法。


看到OP对他用的SMTPClient很迷惑,我想再补充几行。

你需要问一个问题:这个客户端是打开一个到 SMTP 服务器的通道并保留它以供长期使用,还是它只通过它发送 1 条消息并需要在之后关闭。

一些客户端具有一次性使用的核心功能,其他客户端则基于此核心行为并添加一个预先打开的一次性连接池。核心函数 class 既可以用作静态函数,给定资源作为参数,也可以用作单例函数(如果它允许初始化资源而不是连接本身)。这两种情况仅在使用时才需要打开到 SMTP 服务器的通道,这会导致延迟。最后,如果它必须在使用后关闭,那么核心功能就不能作为单例使用,因为我们在服务的整个生命周期中都需要活着。

另一方面,如果客户端使用连接池,则毫无疑问它将成为单例并积极影响用户体验。如果项目的使用环境中没有其他当前库可用,这里的旁注将实现自己的 class 具有此连接池。