在.NET MVC项目中,静态类和单例模式有什么区别?

6

我理解静态类和单例模式的区别,重要的是单例只能被实例化一次,而静态类不需要实例。

这个问题的视角是一个.NET MVC项目,帮助我在它们之间做出决策。

假设我有像下面给出的例子那样的类方法:

  1. 我有一个像ConvertMeterToMiles(int mtr)这样的方法,其中没有依赖注入。

  2. 或者像SendEmail(str eaddress)这样的方法,其中没有依赖注入,但它实例化了new SMTPClient...,然后在finally中将SMTPClient丢弃。

假设我想把这个方法放到实用程序服务类中,那么应该创建一个静态类还是单例(当然要进行依赖注入)?

我知道没有范围或瞬态的意义,因为没有受益于新实例。


1
最佳实践是定义一个非静态类,然后利用依赖注入来管理生命周期,https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-6.0#lifetime-and-registration-options。 - Lex Li
如果您的方法实例化了一个 SMTPClient,那么它就有一个依赖关系,但是您不会将其显示给外部世界。相反,它应该通过构造函数注入来获取该对象。当构建容器时,必须决定这两个类中的一个或两个类是否应具有瞬态、作用域或单例生命周期。 - Oliver
如果您有仅在给定参数上工作并在给定相同参数时始终产生相同结果的方法,则称为纯函数,并且可以将其设置为静态。它们的范围应该是公共的、私有的还是受保护的是另一个问题。 - Oliver
另一个提示:在使用 DI 时,默认情况应始终使用瞬态类。仅当一个类持有一些需要比瞬态更长的生命周期的内部状态时,才考虑使用其他作用域。不要担心性能问题,因为“没有必要一遍又一遍地实例化这个类”。这是过早的优化。只有在您可以测量到性能问题时才需要注意其他类的生命周期。 - Oliver
如果您有一个没有依赖项和未使用资源(smtpClient)的方法,则将其放置为静态类。静态方法在实例化时不使用“this”参数:(使用ildsm检查IL代码以查看差异)。如果您在方法调用期间使用资源,则最好通过继承Disposable并将此类注册为作用域或瞬态来使用可处置类,因为这是MVC应用程序,并且在调用API时需要实例。 - Santosh Karanam
7个回答

4
在你的应用程序上下文中,我会说使用DI或不使用DI以及谁来控制生命周期/实例化/注入是有区别的。
如果某些功能不应该根据环境或其他可变性来源而有所不同(还要考虑方法是否纯粹,即纯函数通常也是很好的候选对象),将其移动到一些静态辅助类中可能是相当不错的选择。例如,ConvertMeterToMiles似乎是一个很好的候选对象。
另一方面,SendEmail似乎不是一个好的选择 - 有些环境下你可能不想发送电子邮件(例如测试),或者未来你可能预计会有多个实现(或需要重新实现)这个功能(例如对于某些情况,可以使用队列延迟发送电子邮件,由后台工作人员或其他服务处理)。在这种情况下,你可以充分利用DI的存在,并将此功能封装起来并隐藏在契约后面(同时我还会说,在DI中注册和解析SMTPClient设置时更加清晰)

1
我指的是Mailkit的SmtpClient在线示例,我看到他们在finally块中New SmtpClient并进行处理,而不是通过DI注入它。也许这是为了确保SmtpClient的处理是立即的,而不是依赖于DI框架的处理? - variable
其次,如果实用程序类被注册为单例,则我必须新建 SmtpClient,否则我将不得不声明类和 SmtpClient 为作用域。正确吗? - variable
@variable 我并不建议在 DI 中注册 SmtpClient,而只是为其设置。我的帖子是关于包含示例方法的类,因此在这种情况下,新建/释放 SmtpClient 是实现细节,可以在 DI 容器之外控制 SmtpClient 的生命周期(我认为在某些情况下注册 SmtpClient 可能会引起问题)。 - Guru Stron
1
正如我在第一条评论中提到的那样,我正在使用MailKit的SmtpClient - variable
@variable 抱歉,我错过了! - Guru Stron
如果你提到的 SmtpClient 提供了一个开放通道来连接邮件服务器并在任何时候发送多封邮件,那么你可以将其用作单例模式。另一方面,如果它是一次性使用的可丢弃类,则可以将其用作静态方法或将其包装在自己的类中,以便其行为像单例模式。 - Yılmaz Durmaz

4

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


但我想从 Web API 的角度来问,由于每个请求都由不同的线程处理,创建一个实用类作为单例和静态类之间有什么区别? - variable
1
嗨@variable,Web API或其他项目类型并不相关:这就是区别,一个是实例化的,另一个则没有。 - Pierre Plourde
我知道那部分,但我想从 Web API 的角度理解这个概念。 - variable
3
这里与 Web API 没有任何关系。线程在这里并不重要。唯一重要的是,你是否需要实例化该类,这取决于你的设计。请注意不要改变原意。 - glenebob
你的回答只是定义了它们是什么,但上下文对于你使用静态或单例模式非常重要,而OP的问题是关于在Web API方面的使用。 - Yılmaz Durmaz
显示剩余3条评论

2
你所说的单例(singleton)只能实例化一次,因此它是保持对象存活的好地方,这些对象可以在应用程序生命周期内使用。对于mvc项目,单例对象对于每个请求都是相同的。
在你的第二种方法中,你的SmtpClient不需要每次创建一个新实例并进行处理。
从msdn文档中可以看出:
“SmtpClient”类的实现池化SMTP连接,以便它可以避免为了向同一服务器发送每个消息而重新建立连接的开销。应用程序可以重复使用相同的SmtpClient对象,向同一SMTP服务器和许多不同的SMTP服务器发送许多不同的电子邮件。因此,无法确定应用程序何时完成使用SmtpClient对象并且应该清除它。
因此,它是单例实用程序服务的好候选者。
单例模式将如下所示:
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的singleton,这样您的SmtpClient将仅被实例化一次。
顺便说一下,Microsoft不建议使用SmtpClient(在某些平台上已过时,在其他平台上也不推荐使用),所以不确定它是否是一个好选择 :/
关于您的第一个方法ConvertMeterToMiles(int mtr),它只是一个转换,每次只需进行一次计算。它不需要任何属性,并且不需要实例。因此,完全静态类是个好选择。
public static class MeterHelper
{
    public static decimal ConvertMeterToMiles(int mtr)
    {
        return mtr * 0.0006213712;
    }
}


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

SmtpClient可以进行注入,而不是直接New一个实例吗? - variable
我不知道 SmtpClient 是如何工作的。它似乎没有任何接口(如 ISmtpClient),所以我不确定,当您添加服务时可能需要在启动时进行新建。 - Anor
请问 "startup" 中的 "new" 是什么意思? - variable
这是一种服务注册方法,在此链接的第二个示例中msdn - Anor
使用后它也会自动释放吗? - variable

1
首先,让我简单回顾一下:静态类不能有实例,因此您需要使用类的名称和单例类只能有一个实例供其他人共享。
对于静态类,您需要将其包含在代码文件中。对于单例类,您将首先创建一个实例,然后将其传递给参数列表中的其他方法。
在aspnet的上下文中,由于我们只有一个实例,我们将其创建和处理留给框架,使用services.AddSingleton<ISingleton, Singleton>();将其传递给控制器public SomeController(ISingleton singleton)。当访问者访问此控制器端点时,他们的所有请求都将是不同的,但会由此单个实例处理。 aspnet将通过它们的接口确定控制器需要哪些单例,并仅注入那些请求的单例。
无论是服务器端全局状态持有者、数据库连接器还是您的电子邮件发送器,在您实现的所有活动都将通过此单例实例进行。您可以将负载均衡器实现到其中,以便请求可以在没有瓶颈的情况下进行处理。
另一方面,对于静态类,您将更喜欢短暂的方法,因为它们将分别运行每个对控制器的请求。距离转换器就是这样的方法。它不需要执行任何长时间的过程,也不依赖于其他昂贵的资源。但是,您可能希望缓存最频繁的计算并从缓存发送响应,然后将此距离转换器转换为使用资源较长时间的单例将是一个更好的想法。
因此,简而言之,根据资源的使用情况,您将更喜欢具有大量昂贵操作的长时间方法或短暂独立方法。
看到OP对他使用的SMTPClient存在困惑,我想再添加几行。
您需要问一个问题:这个客户端是否打开了通往SMTP服务器的通道并在长时间使用它,还是仅向其发送1条消息并在使用后关闭。
一些客户端具有一次性使用核心功能,其他客户端则构建在此核心行为之上并添加了一组预打开的单次使用连接池。核心功能类可以同时用作静态类(给定资源作为参数)或作为单例类(如果它允许具有初始化资源而不是连接本身)。在使用时,这两种情况都需要仅打开一次通往SMTP服务器的通道,并且这会导致延迟。最后,如果必须在使用后关闭它,则核心功能不能用作单例,因为我们需要在服务的整个生命周期内保持其活动状态。
另一方面,如果客户端使用连接池,则无论如何它都将是单例,并且会积极影响用户体验。这里的一个附注是,在项目的使用环境中没有其他当前库可用时,实现自己的类并具有此连接池。

1

静态类是单例模式。使用静态字段或通过依赖注入创建单例的唯一区别在于如何访问它,仅此而已。

但在SmtpClient的上下文中,它不是线程安全的。实际上,文档中说:

注意

如果正在进行电子邮件传输并且再次调用SendAsyncSend,则会收到InvalidOperationException

换句话说,您不能使用相同的SmtpClient实例同时发送两封电子邮件。因此,将SmtpClient用作任何类型的单例都不是一个好主意。最好将其作用域化,或者根本不使用DI,并在需要时声明一个新的实例。


0
我建议使用注入的单例模式。从功能上讲,这几乎没有什么区别,但是在测试方面,注入单例模式有很大的优势。
虽然静态方法本身很容易测试,但是独立测试使用它们的代码变得非常困难。
以您的SendEmail(str eaddress)示例为例。如果我们将其实现为静态帮助方法,那么在不创建真实SMTPClient的情况下,将无法对使用此方法的代码进行单元测试。相比之下,如果我们注入一个带有接口的单例帮助类,则可以在调用SendEmail的代码进行测试时模拟该接口。

0

单例模式和静态类在功能上没有区别。

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

如果只是使用像Sum(int a, int b)这样没有任何状态的工具方法,则不会有任何问题。

在这种情况下,除了需要注入单例之外,两者基本上没有区别。即使涉及 Web API,也没有什么特别之处。

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


静态类无法保存活动数据。每次使用它们的方法都必须进行初始化。单例在开始时初始化资源并保持其活动状态。这会产生很大的差异。 - Yılmaz Durmaz
@YılmazDurmaz 实际上,方法只是存储在内存中等待使用。它们不需要初始化,因为没有实例可以开始。这就是为什么它们被推荐用于实用程序代码的原因。那么,您所说的活动数据是什么意思? - Ladrillo
我的意思是,对于静态类,你可以将所有需要的内容硬编码到类/方法中,或者每次使用它们时都作为参数传递。另一方面,对于单例模式,除了硬编码的内容(如域名),你还需要通过一些参数(如端口、代理)进行初始化。然后在其生命周期内,你可以“更改”其中一些参数,比如SMTP服务器,最后只需将经常变化的参数作为参数传递,例如要发送电子邮件的电子邮件地址和消息。这样,单例就成为了一个活动对象。 - Yılmaz Durmaz
@YılmazDurmaz,你也可以使用静态类来实现。 虽然这是可行的,但并不意味着这是一个好主意。我更喜欢将静态类留给像全局只读数据或者在我的情况下,一个ValidationHelper这样的简单东西,它只是一个使用数据注释检查表单有效性的算法 :) 其他所有的东西我都使用单例! - Ladrillo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接