Autofac 和 NLog 的日志记录模块是如何工作的?
How does the logging module for Autofac and NLog work?
我对 Autofac 和 Nlog 还是很陌生,我需要一些帮助来理解我的 Nlog 的 Autofac LoggingModule 中发生了什么。由于遵循 injecting-nlog-with-autofacs-registergeneric,它按预期工作。但不仅仅是复制粘贴,我想确保我了解每种方法中发生的事情 (Load & AttachToComponentRegistration)。如果你能回顾我的想法并进一步澄清我不正确的地方(我敢肯定有一点),我将不胜感激。提前致谢!
- 使用 Nlog 的数据库目标
- 使用 Autofac 的依赖注入
- ASP.NET 用于学习的 MVC 网络应用程序
- Dvd 库应用程序(DvdAdd、DvdEdit、DvdDelete、DvdList)
日志模块
public class LoggingModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder
.Register((c, p) => new LogService(p.TypedAs<Type>()))
.AsImplementedInterfaces();
}
protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration)
{
registration.Preparing +=
(sender, args) =>
{
var forType = args.Component.Activator.LimitType;
var logParameter = new ResolvedParameter(
(p, c) => p.ParameterType == typeof(ILog),
(p, c) => c.Resolve<ILog>(TypedParameter.From(forType)));
args.Parameters = args.Parameters.Union(new[] { logParameter });
};
}
}
我对里面代码的理解Load()
c - 提供给表达式的参数 c 是组件上下文(一个 IComponentContext 对象)正在其中创建组件。可以访问服务或解决组件依赖关系的上下文。
p - 传入参数集
的IEnumerable
AsImplementedInterfaces - Autofac 允许其用户显式或隐式注册类型。 “As”用于显式注册,“AsImplementedInterfaces”和“AsSelf”用于隐含的。换句话说,容器会针对它实现的所有接口自动注册实现。
思考:Load方法代码注册了一个新的LogServiceclass(表示“c”),类型为记录器(表示“p”)作为 LogService class
的构造函数参数
问题:
- 以上我的想法是否正确?
- 它应该是 SingleInstance 还是应该/它只会在调用 classes 范围内存在? (我在考虑我的工作单位)
我对里面代码的理解AttachToComponentRegistration()
AttachToComponentRegistration 方法 - 覆盖以将特定于模块的功能附加到组件注册。
AttachToComponentRegistration 参数:
- IComponentRegistry componentRegistry - 根据其提供的服务提供组件注册。
- IComponentRegistration registration - 描述容器内的逻辑组件。
registration.Preparing - 需要新实例时触发。可以通过在提供的事件参数中设置实例 属性 来提供实例以跳过常规激活器。
var forType = args.Component.Activator.LimitType;
args = Autofac.Core.PreparingEventArgs - 在激活过程之前触发以允许更改参数或提供替代实例。
组件 = PreparingEventArgs.Component 属性 - 获取提供被激活实例的组件
Activator = IComponentRegistration.Activator 属性 - 获取用于创建实例的激活器。
LimitType = IInstanceActivator.LimitType 属性 - 获取组件实例已知的最具体类型可转换为。
Thoughts on forType
- 据我了解,此变量包含调用 [=248= 的 Name
和 FullName
] 从哪里调用日志服务?
forType Debugger Image
问题:
- 我的想法
forType
正确吗?
var logParameter = new ResolvedParameter(
(p, c) => p.ParameterType == typeof(ILog),
(p, c) => c.Resolve<ILog>(TypedParameter.From(forType)));
ResolvedParameter - 可用作提供从容器动态检索的值的方式,
例如通过按名称解析服务。
关于 logParameter
的想法 - 这是我开始迷路的地方。同样,它检查参数是否为 ILog 类型,如果是,它将使用构造函数参数解析它并传入 forType
变量?
问题:
- 以上
logParameter
我的想法是否正确?
args.Parameters = args.Parameters.Union(new[] { logParameter });
args.Parameters = PreparingEventArgs.Parameters 属性 - 获取或设置提供给激活器的参数。
args.Parameters.Union = 使用默认相等比较器生成两个序列的集合并集。 Returns 一个 System.Collections.Generic.IEnumerable`1 包含来自两个输入序列的元素,不包括重复元素。
关于 args.Parameters
的想法 - 除了猜测它 returns 参数集合并删除重复项外,我现在真的不知道这一点?
问题:
- 你能帮我谈谈
args.Parameters
中发生的事情吗?
logParameter Debugger Image
Nlog Database Table Image
日志服务class
public class LogService : ILog
{
private readonly ILogger _log;
public LogService(Type type)
{
_log = LogManager.GetLogger(type.FullName);
}
public void Debug(string message, params object[] args)
{
Log(LogLevel.Debug, message, args);
}
public void Info(string message, params object[] args)
{
Log(LogLevel.Info, message, args);
}
public void Warn(string message, params object[] args)
{
Log(LogLevel.Warn, message, args);
}
public void Error(string message, params object[] args)
{
Log(LogLevel.Error, message, args);
}
public void Error(Exception ex)
{
Log(LogLevel.Error, null, null, ex);
}
public void Error(Exception ex, string message, params object[] args)
{
Log(LogLevel.Error, message, args, ex);
}
public void Fatal(Exception ex, string message, params object[] args)
{
Log(LogLevel.Fatal, message, args, ex);
}
private void Log(LogLevel level, string message, object[] args)
{
_log.Log(typeof(LogService), new LogEventInfo(level, _log.Name, null, message, args));
}
private void Log(LogLevel level, string message, object[] args, Exception ex)
{
_log.Log(typeof(LogService), new LogEventInfo(level, _log.Name, null, message, args, ex));
}
}
ILog 界面
public interface ILog
{
void Debug(string message, params object[] args);
void Info(string message, params object[] args);
void Warn(string message, params object[] args);
void Error(string message, params object[] args);
void Error(Exception ex);
void Error(Exception ex, string message, params object[] args);
void Fatal(Exception ex, string message, params object[] args);
}
这里有很多要打开。您实际上并不是在寻求特定问题的答案,而是代码演练和对现有有效解决方案的解释,因此如果您需要的远不止我要说的,我可能会建议您发帖至 StackExchange Code Review在这里给你。并不是想无益,而是,如果你的问题是 "Is my thinking right?" 而答案是 "sort of,",那么对于每个单独的点都有很多讨论来解释为什么 "sort of" 是答案(或 "no," 或 "yes," 视情况而定)。它可以变成一个冗长的答案,然后是其他需要澄清的问题,这需要额外的答案......而且 Whosebug 并不是一个真正能够进行此类事情的讨论论坛。
[也就是说,我可能会花一个小时在这里写下答案...但我不能保证我真的会回来跟进任何事情,因为还有其他问题需要回答和其他我需要分配时间的事情。 Whosebug 实际上更多地是关于 "How do I...?" 或其他具有单一、合理具体答案的事物。]
首先,我建议您在一些断点上使用调试器深入研究,以实际了解发生了什么。例如,您询问某个区域的 LimitType
中有什么 - 您只需在该行上放置一个断点并查看值即可轻松回答该问题。这将是您自己进行进一步说明的好方法 - 胜利的断点。
其次,我建议花一些时间 with the Autofac docs。 那里有很多文档可以回答问题。
- 这里的 NLog 模块似乎是基于 the log4net module in the documentation 的,它对发生的事情有更多的解释。
- 有 an explanation of parameters (like
TypedParameter
) and how they're used.
鉴于文档可以完善一些可能不清楚的内容,与其尝试解决每个 "are my thoughts correct" 项目,不如让我对模块进行大量注释 和希望澄清事情。
// General module documentation is here:
// https://autofac.readthedocs.io/en/latest/configuration/modules.html
public class LoggingModule : Module
{
// Load basically registers types with the container just like
// if you were doing it yourself on the ContainerBuilder. It's
// just a nice way of packaging up a set of registrations so
// they're not all in your program's "Main" method or whatever.
protected override void Load(ContainerBuilder builder)
{
// This is a lambda registration. Docs here:
// https://autofac.readthedocs.io/en/latest/register/registration.html#lambda-expression-components
// This one uses both the component context (c) and the incoming
// set of parameters (p). In this lambda, the parameters are NOT the set of constructor
// parameters that Autofac has resolved - they're ONLY things that
// were MANUALLY specified. In this case, it's assuming a TypedParameter
// with a System.Type value is being provided manually. It's not going
// to try resolving that value from the container. This is going hand-in-hand
// with the logParameter you see in AttachToComponentRegistration.
// Parameter docs are here:
// https://autofac.readthedocs.io/en/latest/resolve/parameters.html
// In general if you resolve something that has both manually specified parameters
// and things that can be resolved by Autofac, the manually specified parameters
// will take precedence. However, in this lambda it's very specifically looking
// for a manually specified parameter.
// You'll want to keep this as a default InstancePerDependency because you probably
// want this to live as long as the thing using it and no longer. Likely
// NLog already has object pooling and caching built in so this isn't as
// expensive as you think, but I'm no NLog expert. log4net does handle
// that for you.
builder
.Register((c, p) => new LogService(p.TypedAs<Type>()))
.AsImplementedInterfaces();
}
// This method attaches a behavior (in this case, an event handler) to every
// component registered in the container. Think of it as a way to run a sort
// of "global foreach" over everything registered.
protected override void AttachToComponentRegistration(
IComponentRegistry componentRegistry,
IComponentRegistration registration)
{
// The Preparing event is called any time a new instance is needed. There
// are docs for the lifetime events but Preparing isn't on there. Here are the
// docs and the issue I filed on your behalf to get Preparing documented.
// https://autofac.readthedocs.io/en/latest/lifetime/events.html
// https://github.com/autofac/Documentation/issues/69
// You can see the Preparing event here:
// https://github.com/autofac/Autofac/blob/6dde84e5b0a3f82136a0567a84da498b04e1fa2d/src/Autofac/Core/IComponentRegistration.cs#L83
// and the event args here:
// https://github.com/autofac/Autofac/blob/6dde84e5b0/src/Autofac/Core/PreparingEventArgs.cs
registration.Preparing +=
(sender, args) =>
{
// The Component is the thing being resolved - the thing that
// needs a LogService injected. The Component.Activator is the
// thing that is actually going to execute to "new up" an instance
// of the Component. The Component.Activator.LimitType is the actual
// System.Type of the thing being resolved.
var forType = args.Component.Activator.LimitType;
// The docs above explain ResolvedParameter - basically a manually
// passed in parameter that can execute some logic to determine if
// it satisfies a constructor or property dependency. The point of
// this particular parameter is to provide an ILog to anything being
// resolved that happens to have an ILog constructor parameter.
var logParameter = new ResolvedParameter(
// p is the System.Reflection.ParameterInfo that describes the
// constructor parameter that needs injecting. c is the IComponentContext
// in which the resolution is being done (not used here). If this
// method evaluates to true then this parameter will be used; if not,
// it will refuse to provide a value. In this case, if the parameter
// being injected is an ILog, this ResolvedParameter will tell Autofac
// it can provide a value.
(p, c) => p.ParameterType == typeof(ILog),
// p and c are the same here, but this time they're used to actually
// generate the value of the parameter - the ILog instance that should
// be injected. Again, this will only run if the above predicate evaluates
// to true. This creates an ILog by manually resolving from the same
// component context (the same lifetime scope) as the thing that
// needs the ILog. Remember earlier that call to p.AsTyped<Type>()
// to get a parameter? The TypedParameter thing here is how that
// value gets poked in up there. This Resolve call will effectively
// end up calling the lambda registration.
(p, c) => c.Resolve<ILog>(TypedParameter.From(forType)));
// The thing being resolved (the component that consumes ILog) now
// needs to be told to make use of the log parameter, so add it into
// the list of parameters that can be used when resolving that thing.
// If there's an ILog, Autofac will use this specified parameter to
// fulfill the requirement.
args.Parameters = args.Parameters.Union(new[] { logParameter });
};
}
}
log4net module example is the ability to do property injection for the logger. However, I'm not going to solve that here; you can look at the example right in the documentation 中缺少的内容,如果您需要该功能,可以将其作为练习。
希望对您有所帮助。我可能不会回来跟进其他问题,所以如果这还不够,我非常非常建议设置一些断点,也许设置一些微小的最小重现单元测试,诸如此类,以及做一些更深入的探索以获得清晰度。老实说,让别人解释它是一回事,但实际看到它 并深入研究各种项目的源代码是另一回事。您会对后一种方法有更全面的了解,即使它可能没有那么快。
我对 Autofac 和 Nlog 还是很陌生,我需要一些帮助来理解我的 Nlog 的 Autofac LoggingModule 中发生了什么。由于遵循 injecting-nlog-with-autofacs-registergeneric,它按预期工作。但不仅仅是复制粘贴,我想确保我了解每种方法中发生的事情 (Load & AttachToComponentRegistration)。如果你能回顾我的想法并进一步澄清我不正确的地方(我敢肯定有一点),我将不胜感激。提前致谢!
- 使用 Nlog 的数据库目标
- 使用 Autofac 的依赖注入
- ASP.NET 用于学习的 MVC 网络应用程序
- Dvd 库应用程序(DvdAdd、DvdEdit、DvdDelete、DvdList)
日志模块
public class LoggingModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder
.Register((c, p) => new LogService(p.TypedAs<Type>()))
.AsImplementedInterfaces();
}
protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration)
{
registration.Preparing +=
(sender, args) =>
{
var forType = args.Component.Activator.LimitType;
var logParameter = new ResolvedParameter(
(p, c) => p.ParameterType == typeof(ILog),
(p, c) => c.Resolve<ILog>(TypedParameter.From(forType)));
args.Parameters = args.Parameters.Union(new[] { logParameter });
};
}
}
我对里面代码的理解Load()
c - 提供给表达式的参数 c 是组件上下文(一个 IComponentContext 对象)正在其中创建组件。可以访问服务或解决组件依赖关系的上下文。
p - 传入参数集
的IEnumerableAsImplementedInterfaces - Autofac 允许其用户显式或隐式注册类型。 “As”用于显式注册,“AsImplementedInterfaces”和“AsSelf”用于隐含的。换句话说,容器会针对它实现的所有接口自动注册实现。
思考:Load方法代码注册了一个新的LogServiceclass(表示“c”),类型为记录器(表示“p”)作为 LogService class
的构造函数参数问题:
- 以上我的想法是否正确?
- 它应该是 SingleInstance 还是应该/它只会在调用 classes 范围内存在? (我在考虑我的工作单位)
我对里面代码的理解AttachToComponentRegistration()
AttachToComponentRegistration 方法 - 覆盖以将特定于模块的功能附加到组件注册。
AttachToComponentRegistration 参数:
- IComponentRegistry componentRegistry - 根据其提供的服务提供组件注册。
- IComponentRegistration registration - 描述容器内的逻辑组件。
registration.Preparing - 需要新实例时触发。可以通过在提供的事件参数中设置实例 属性 来提供实例以跳过常规激活器。
var forType = args.Component.Activator.LimitType;
args = Autofac.Core.PreparingEventArgs - 在激活过程之前触发以允许更改参数或提供替代实例。
组件 = PreparingEventArgs.Component 属性 - 获取提供被激活实例的组件
Activator = IComponentRegistration.Activator 属性 - 获取用于创建实例的激活器。
LimitType = IInstanceActivator.LimitType 属性 - 获取组件实例已知的最具体类型可转换为。
Thoughts on forType
- 据我了解,此变量包含调用 [=248= 的 Name
和 FullName
] 从哪里调用日志服务?
forType Debugger Image
问题:
- 我的想法
forType
正确吗?
var logParameter = new ResolvedParameter(
(p, c) => p.ParameterType == typeof(ILog),
(p, c) => c.Resolve<ILog>(TypedParameter.From(forType)));
ResolvedParameter - 可用作提供从容器动态检索的值的方式, 例如通过按名称解析服务。
关于 logParameter
的想法 - 这是我开始迷路的地方。同样,它检查参数是否为 ILog 类型,如果是,它将使用构造函数参数解析它并传入 forType
变量?
问题:
- 以上
logParameter
我的想法是否正确?
args.Parameters = args.Parameters.Union(new[] { logParameter });
args.Parameters = PreparingEventArgs.Parameters 属性 - 获取或设置提供给激活器的参数。
args.Parameters.Union = 使用默认相等比较器生成两个序列的集合并集。 Returns 一个 System.Collections.Generic.IEnumerable`1 包含来自两个输入序列的元素,不包括重复元素。
关于 args.Parameters
的想法 - 除了猜测它 returns 参数集合并删除重复项外,我现在真的不知道这一点?
问题:
- 你能帮我谈谈
args.Parameters
中发生的事情吗?
logParameter Debugger Image Nlog Database Table Image
日志服务class
public class LogService : ILog
{
private readonly ILogger _log;
public LogService(Type type)
{
_log = LogManager.GetLogger(type.FullName);
}
public void Debug(string message, params object[] args)
{
Log(LogLevel.Debug, message, args);
}
public void Info(string message, params object[] args)
{
Log(LogLevel.Info, message, args);
}
public void Warn(string message, params object[] args)
{
Log(LogLevel.Warn, message, args);
}
public void Error(string message, params object[] args)
{
Log(LogLevel.Error, message, args);
}
public void Error(Exception ex)
{
Log(LogLevel.Error, null, null, ex);
}
public void Error(Exception ex, string message, params object[] args)
{
Log(LogLevel.Error, message, args, ex);
}
public void Fatal(Exception ex, string message, params object[] args)
{
Log(LogLevel.Fatal, message, args, ex);
}
private void Log(LogLevel level, string message, object[] args)
{
_log.Log(typeof(LogService), new LogEventInfo(level, _log.Name, null, message, args));
}
private void Log(LogLevel level, string message, object[] args, Exception ex)
{
_log.Log(typeof(LogService), new LogEventInfo(level, _log.Name, null, message, args, ex));
}
}
ILog 界面
public interface ILog
{
void Debug(string message, params object[] args);
void Info(string message, params object[] args);
void Warn(string message, params object[] args);
void Error(string message, params object[] args);
void Error(Exception ex);
void Error(Exception ex, string message, params object[] args);
void Fatal(Exception ex, string message, params object[] args);
}
这里有很多要打开。您实际上并不是在寻求特定问题的答案,而是代码演练和对现有有效解决方案的解释,因此如果您需要的远不止我要说的,我可能会建议您发帖至 StackExchange Code Review在这里给你。并不是想无益,而是,如果你的问题是 "Is my thinking right?" 而答案是 "sort of,",那么对于每个单独的点都有很多讨论来解释为什么 "sort of" 是答案(或 "no," 或 "yes," 视情况而定)。它可以变成一个冗长的答案,然后是其他需要澄清的问题,这需要额外的答案......而且 Whosebug 并不是一个真正能够进行此类事情的讨论论坛。
[也就是说,我可能会花一个小时在这里写下答案...但我不能保证我真的会回来跟进任何事情,因为还有其他问题需要回答和其他我需要分配时间的事情。 Whosebug 实际上更多地是关于 "How do I...?" 或其他具有单一、合理具体答案的事物。]
首先,我建议您在一些断点上使用调试器深入研究,以实际了解发生了什么。例如,您询问某个区域的 LimitType
中有什么 - 您只需在该行上放置一个断点并查看值即可轻松回答该问题。这将是您自己进行进一步说明的好方法 - 胜利的断点。
其次,我建议花一些时间 with the Autofac docs。 那里有很多文档可以回答问题。
- 这里的 NLog 模块似乎是基于 the log4net module in the documentation 的,它对发生的事情有更多的解释。
- 有 an explanation of parameters (like
TypedParameter
) and how they're used.
鉴于文档可以完善一些可能不清楚的内容,与其尝试解决每个 "are my thoughts correct" 项目,不如让我对模块进行大量注释 和希望澄清事情。
// General module documentation is here:
// https://autofac.readthedocs.io/en/latest/configuration/modules.html
public class LoggingModule : Module
{
// Load basically registers types with the container just like
// if you were doing it yourself on the ContainerBuilder. It's
// just a nice way of packaging up a set of registrations so
// they're not all in your program's "Main" method or whatever.
protected override void Load(ContainerBuilder builder)
{
// This is a lambda registration. Docs here:
// https://autofac.readthedocs.io/en/latest/register/registration.html#lambda-expression-components
// This one uses both the component context (c) and the incoming
// set of parameters (p). In this lambda, the parameters are NOT the set of constructor
// parameters that Autofac has resolved - they're ONLY things that
// were MANUALLY specified. In this case, it's assuming a TypedParameter
// with a System.Type value is being provided manually. It's not going
// to try resolving that value from the container. This is going hand-in-hand
// with the logParameter you see in AttachToComponentRegistration.
// Parameter docs are here:
// https://autofac.readthedocs.io/en/latest/resolve/parameters.html
// In general if you resolve something that has both manually specified parameters
// and things that can be resolved by Autofac, the manually specified parameters
// will take precedence. However, in this lambda it's very specifically looking
// for a manually specified parameter.
// You'll want to keep this as a default InstancePerDependency because you probably
// want this to live as long as the thing using it and no longer. Likely
// NLog already has object pooling and caching built in so this isn't as
// expensive as you think, but I'm no NLog expert. log4net does handle
// that for you.
builder
.Register((c, p) => new LogService(p.TypedAs<Type>()))
.AsImplementedInterfaces();
}
// This method attaches a behavior (in this case, an event handler) to every
// component registered in the container. Think of it as a way to run a sort
// of "global foreach" over everything registered.
protected override void AttachToComponentRegistration(
IComponentRegistry componentRegistry,
IComponentRegistration registration)
{
// The Preparing event is called any time a new instance is needed. There
// are docs for the lifetime events but Preparing isn't on there. Here are the
// docs and the issue I filed on your behalf to get Preparing documented.
// https://autofac.readthedocs.io/en/latest/lifetime/events.html
// https://github.com/autofac/Documentation/issues/69
// You can see the Preparing event here:
// https://github.com/autofac/Autofac/blob/6dde84e5b0a3f82136a0567a84da498b04e1fa2d/src/Autofac/Core/IComponentRegistration.cs#L83
// and the event args here:
// https://github.com/autofac/Autofac/blob/6dde84e5b0/src/Autofac/Core/PreparingEventArgs.cs
registration.Preparing +=
(sender, args) =>
{
// The Component is the thing being resolved - the thing that
// needs a LogService injected. The Component.Activator is the
// thing that is actually going to execute to "new up" an instance
// of the Component. The Component.Activator.LimitType is the actual
// System.Type of the thing being resolved.
var forType = args.Component.Activator.LimitType;
// The docs above explain ResolvedParameter - basically a manually
// passed in parameter that can execute some logic to determine if
// it satisfies a constructor or property dependency. The point of
// this particular parameter is to provide an ILog to anything being
// resolved that happens to have an ILog constructor parameter.
var logParameter = new ResolvedParameter(
// p is the System.Reflection.ParameterInfo that describes the
// constructor parameter that needs injecting. c is the IComponentContext
// in which the resolution is being done (not used here). If this
// method evaluates to true then this parameter will be used; if not,
// it will refuse to provide a value. In this case, if the parameter
// being injected is an ILog, this ResolvedParameter will tell Autofac
// it can provide a value.
(p, c) => p.ParameterType == typeof(ILog),
// p and c are the same here, but this time they're used to actually
// generate the value of the parameter - the ILog instance that should
// be injected. Again, this will only run if the above predicate evaluates
// to true. This creates an ILog by manually resolving from the same
// component context (the same lifetime scope) as the thing that
// needs the ILog. Remember earlier that call to p.AsTyped<Type>()
// to get a parameter? The TypedParameter thing here is how that
// value gets poked in up there. This Resolve call will effectively
// end up calling the lambda registration.
(p, c) => c.Resolve<ILog>(TypedParameter.From(forType)));
// The thing being resolved (the component that consumes ILog) now
// needs to be told to make use of the log parameter, so add it into
// the list of parameters that can be used when resolving that thing.
// If there's an ILog, Autofac will use this specified parameter to
// fulfill the requirement.
args.Parameters = args.Parameters.Union(new[] { logParameter });
};
}
}
log4net module example is the ability to do property injection for the logger. However, I'm not going to solve that here; you can look at the example right in the documentation 中缺少的内容,如果您需要该功能,可以将其作为练习。
希望对您有所帮助。我可能不会回来跟进其他问题,所以如果这还不够,我非常非常建议设置一些断点,也许设置一些微小的最小重现单元测试,诸如此类,以及做一些更深入的探索以获得清晰度。老实说,让别人解释它是一回事,但实际看到它 并深入研究各种项目的源代码是另一回事。您会对后一种方法有更全面的了解,即使它可能没有那么快。