[ASP.NET MVC 小牛之路]04 - 依赖注入(DI)和Ninject

news/2024/2/28 18:43:27

特别提醒:本文编写时间是 2013 年,请根据目前 .NET 发展接收你所需的知识点。

为什么需要依赖注入

在[ASP.NET MVC 小牛之路]系列的理解MVC模式文章中,我们提到MVC的一个重要特征是关注点分离(separation of concerns)。我们希望应用程序的各部分组件尽可能多的相互独立、尽可能少的相互依赖。

我们的理想情况是:一个组件可以不知道也可以不关心其他的组件,但通过提供的公开接口却可以实现其他组件的功能调用。这种情况就是所谓的松耦合

举个简单的例子。我们要为商品定制一个“高级”的价钱计算器LinqValueCalculator,这个计算器需要实现IValueCalculator接口。如下代码所示:

public interface IValueCalculator {decimal ValueProducts(params Product[] products);
}public class LinqValueCalculator : IValueCalculator {public decimal ValueProducts(params Product[] products) {return products.Sum(p => p.Price);}
}

Product类和前两篇博文中用到的是一样的。现在有个购物车ShoppingCart类,它需要有一个能计算购物车内商品总价钱的功能。但购物车本身没有计算的功能,因此,购物车要嵌入一个计算器组件,这个计算器组件可以是LinqValueCalculator组件,但不一定是LinqValueCalculator组件(以后购物车升级,可能会嵌入别的更高级的计算器)。那么我们可以这样定义购物车ShoppingCart类:

 1 public class ShoppingCart {
 2     //计算购物车内商品总价钱
 3     public decimal CalculateStockValue() {
 4         Product[] products = { 
 5             new Product {Name = "西瓜", Category = "水果", Price = 2.3M}, 
 6             new Product {Name = "苹果", Category = "水果", Price = 4.9M}, 
 7             new Product {Name = "空心菜", Category = "蔬菜", Price = 2.2M}, 
 8             new Product {Name = "地瓜", Category = "蔬菜", Price = 1.9M} 
 9         };
10         IValueCalculator calculator = new LinqValueCalculator();
11 
12         //计算商品总价钱 
13         decimal totalValue = calculator.ValueProducts(products);
14 
15         return totalValue;
16     }
17 }

ShoppingCart类是通过IValueCalculator接口(而不是通过LinqValueCalculator)来计算商品总价钱的。如果以后购物车升级需要使用更高级的计算器,那么只需要改变第10行代码中new后面的对象(即把LinqValueCalculator换掉),其他的代码都不用变动。这样就实现了一定的松耦合。这时三者的关系如下图所示:

这个图说明,ShoppingCart类既依赖IValueCalculator接口又依赖LinqValueCalculator类。这样就有个问题,用现实世界的话来讲就是,如果嵌入在购物车内的计算器组件坏了,会导致整个购物车不能正常工作,岂不是要把整个购物车要换掉!最好的办法是将计算器组件和购物车完全独立开来,这样不管哪个组件坏了,只要换对应的组件即可。即我们要解决的问题是,要让ShoppingCart组件和LinqValueCalculator组件完全断开关系,而依赖注入这种设计模式就是为了解决这种问题。

什么是依赖注入

上面实现的部分松耦合显然并不是我们所需要的。我们所需要的是,在一个类内部,不通过创建对象的实例而能够获得某个实现了公开接口的对象的引用。这种“需要”,就称为DI(依赖注入,Dependency Injection),和所谓的IoC(控制反转,Inversion of Control )是一个意思。

DI是一种通过接口实现松耦合的设计模式。初学者可能会好奇网上为什么有那么多技术文章对DI这个东西大兴其笔,是因为DI对于基于几乎所有框架下,要高效开发应用程序,它都是开发者必须要有的一个重要的理念,包括MVC开发。它是解耦的一个重要手段。

DI模式可分为两个部分。一是移除对组件(上面示例中的LinqValueCalculator)的依赖,二是通过类的构造函数(或类的Setter访问器)来传递实现了公开接口的组件的引用。如下面代码所示:

public class ShoppingCart {IValueCalculator calculator;//构造函数,参数为实现了IValueCalculator接口的类的实例public ShoppingCart(IValueCalculator calcParam) {calculator = calcParam;}//计算购物车内商品总价钱public decimal CalculateStockValue() {Product[] products = { new Product {Name = "西瓜", Category = "水果", Price = 2.3M}, new Product {Name = "苹果", Category = "水果", Price = 4.9M}, new Product {Name = "空心菜", Category = "蔬菜", Price = 2.2M}, new Product {Name = "地瓜", Category = "蔬菜", Price = 1.9M} };//计算商品总价钱 decimal totalValue = calculator.ValueProducts(products);return totalValue;}
}

这样我们就彻底断开了ShoppingCart和LinqValueCalculator之间的依赖关系。某个实现了IValueCalculator接口的类(示例中的LinqValueCalculator)的实例引用作为参数,传递给ShoppingCart类的构造函数。但是ShoppingCart类不知道也不关心这个实现了IValueCalculator接口的类是什么,更没有责任去操作这个类。 这时我们可以用下图来描述ShoppingCart、LinqValueCalculator和IValueCalculator之间的关系:

在程序运行的时候,依赖被注入到ShoppingCart,这个依赖就是,通过ShoppingCart构造函数传递实现了IValueCalculator接口的类的实例引用。在程序运行之前(或编译时),ShoppingCart和任何实现IValueCalculator接口的类没有任何依赖关系。(注意,程序运行时是有具体依赖关系的。)

注意,上面示例使用的注入方式称为“构造注入”,我们也可以通过属性来实现注入,这种注入被称为“setter 注入”,就不举例了,朋友们可以看看T2噬菌体的文章依赖注入那些事儿来对DI进行更多的了解。

由于经常会在编程时使用到DI,所以出现了一些DI的辅助工具(或叫DI容器),如Unity和Ninject等。由于Ninject的轻量和使用简单,加上本人只用过Ninject,所以本系列文章选择用它来开发MVC应用程序。下面开始介绍Ninject,但在这之前,先来介绍一个安装Ninject需要用到的插件-NuGet。

使用NuGet安装库

NuGet 是一种 Visual Studio 扩展,它能够简化在 Visual Studio 项目中添加、更新和删除库(部署为程序包)的操作。比如你要在项目中使用Log4Net这个库,如果没有NuGet这个扩展,你可能要先到网上搜索Log4Net,再将程序包的内容解压缩到解决方案中的特定位置,然后在各项目工程中依次添加程序集引用,最后还要使用正确的设置更新 web.config。而NuGet可以简化这一切操作。例如我们在讲依赖注入的项目中,若要使用一个NuGet库,可直接右击项目(或引用),选择“管理NuGet程序包”(VS2010下为“Add Library Package Reference”),如下图:

在弹出如下窗口中选择“联机”,搜索“Ninject”,然后进行相应的操作即可:

在本文中我们只需要知道如何使用NuGet来安装库就可以了。NuGet的详细使用方法可查看MSDN文档:使用 NuGet 管理项目库。

使用Ninject的一般步骤

在使用Ninject前先要创建一个Ninject内核对象,代码如下:

class Program { static void Main(string[] args) { //创建Ninject内核实例IKernel ninjectKernel = new StandardKernel(); } 
}

使用Ninject内核对象一般可分为两个步骤。第一步是把一个接口(IValueCalculator)绑定到一个实现该接口的类(LinqValueCalculator),如下:

...
//绑定接口到实现了该接口的类 ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator<();
...

这个绑定操作就是告诉Ninject,当接收到一个请求IValueCalculator接口的实现时,就返回一个LinqValueCalculator类的实例。

第二步是用Ninject的Get方法去获取IValueCalculator接口的实现。这一步,Ninject将自动为我们创建LinqValueCalculator类的实例,并返回该实例的引用。然后我们可以把这个引用通过构造函数注入到ShoppingCart类。如下代码所示:

...
// 获得实现接口的对象实例
IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>(); 
// 创建ShoppingCart实例并注入依赖
ShoppingCart cart = new ShoppingCart(calcImpl); 
// 计算商品总价钱并输出结果
Console.WriteLine("Total: {0:c}", cart.CalculateStockValue());
...

Ninject的使用的一般步骤就是这样。该示例可正确输出如下结果:

但看上去Ninject的使用好像使得编码变得更加烦琐,朋友们会问,直接使用下面的代码不是更简单吗:

...
IValueCalculator calcImpl = new LinqValueCalculator();
ShoppingCart cart = new ShoppingCart(calcImpl);
Console.WriteLine("Total: {0:c}", cart.CalculateStockValue());
...

的确,对于单个简单的DI,用Ninject确实显得麻烦。但如果添加多个复杂点的依赖关系,使用Ninject则可大大提高编码的工作效率。

Ninject如何提高编码效率

当我们请求Ninject创建某个类型的实例时,它会检查这个类型和其它类型之间的耦合关系。如果存在依赖关系,那么Ninject会根据依赖处理理它们,并创建所有所需类的实例。为了解释这句话和说明使用Ninject编码的便捷,我们再创建一个接口IDiscountHelper和一个实现该接口的类DefaultDiscountHelper,代码如下:

//折扣计算接口
public interface IDiscountHelper {decimal ApplyDiscount(decimal totalParam);
}//默认折扣计算器
public class DefaultDiscountHelper : IDiscountHelper {public decimal ApplyDiscount(decimal totalParam) {return (totalParam - (1m / 10m * totalParam));}
}

IDiscounHelper接口声明了ApplyDiscount方法,DefaultDiscounterHelper实现了该接口,并定义了打9折的ApplyDiscount方法。然后我们可以把IDiscounHelper接口作为依赖添加到LinqValueCalculator类中。代码如下:

public class LinqValueCalculator : IValueCalculator { private IDiscountHelper discounter; public LinqValueCalculator(IDiscountHelper discountParam) { discounter = discountParam; } public decimal ValueProducts(params Product[] products) { return discounter.ApplyDiscount(products.Sum(p => p.Price)); } 
}

LinqValueCalculator类添加了一个用于接收IDiscountHelper接口的实现的构造函数,然后在ValueProducts方法中调用该接口的ApplyDiscount方法对计算出的商品总价钱进行打折处理,并返回折后总价。

到这,我们先来画个图理一理ShoppingCart、LinqValueCalculator、IValueCalculator以及新添加的IDiscountHelper和DefaultDiscounterHelper之间的关系:

以此,我们还可以添加更多的接口和实现接口的类,接口和类越来越多时,它们的关系图看上去会像一个依赖“链”,和生物学中的分子结构图差不多。

按照前面说的使用Ninject的“二个步骤”,现在我们在Main中的方法中编写用于计算购物车中商品折后总价钱的代码,如下所示:

 1 class Program {
 2     static void Main(string[] args) {
 3         IKernel ninjectKernel = new StandardKernel();
 4 
 5         ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
 6         ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>();
 7 
 8         IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>();
 9         ShoppingCart cart = new ShoppingCart(calcImpl);
10         Console.WriteLine("Total: {0:c}", cart.CalculateStockValue());
11         Console.ReadKey();
12     }
13 }

输出结果:

代码一目了然,虽然新添加了一个接口和一个类,但Main方法中只增加了第6行一句代码,获取实现IValueCalculator接口的对象实例的代码不需要做任何改变。
定位到代码的第8行,这一行代码,Ninject为我们做的事是:
  当我们需要使用IValueCalculator接口的实现时(通过Get方法),它便为我们创建LinqValueCalculator类的实例。而当创建LinqValueCalculator类的实例时,它检查到这个类依赖IDiscountHelper接口。于是它又创建一个实现了该接口的DefaultDiscounterHelper类的实例,并通过构造函数把该实例注入到LinqValueCalculator类。然后返回LinqValueCalculator类的一个实例,并赋值给IValueCalculator接口的对象(第8行的calcImpl)。

总之,不管依赖“链”有多长有多复杂,Ninject都会按照上面这种方式检查依赖“链”上的每个接口和实现接口的类,并自动创建所需要的类的实例。在依赖“链”越长越复杂的时候,更能显示使用Ninject编码的高效率。

Ninject的绑定方式

我个人将Ninject的绑定方式分为:一般绑定、指定值绑定、自我绑定、派生类绑定和条件绑定。这样分类有点牵强,只是为了本文的写作需要和方便读者阅读而分,并不是官方的分类

1、一般绑定

在前文的示例中用Bind和To方法把一个接口绑定到实现该接口的类,这属于一般的绑定。通过前文的示例相信大家已经掌握了,在这就不再累述。

2、指定值绑定

我们知道,通过Get方法,Ninject会自动帮我们创建我们所需要的类的实例。但有的类在创建实例时需要给它的属性赋值,如下面我们改造了一下的DefaultDiscountHelper类:

public class DefaultDiscountHelper : IDiscountHelper { public decimal DiscountSize { get; set; } public decimal ApplyDiscount(decimal totalParam) { return (totalParam - (DiscountSize / 10m * totalParam)); } 
}

给DefaultDiscountHelper类添加了一个DiscountSize属性,实例化时需要指定折扣值(DiscountSize属性值),不然ApplyDiscount方法就没意义。而实例化的动作是Ninject自动完成的,怎么告诉Ninject在实例化类的时候给某属性赋一个指定的值呢?这时就需要用到参数绑定,我们在绑定的时候可以通过给WithPropertyValue方法传参的方式指定DiscountSize属性的值,如下代码所示:

public static void Main() {IKernel ninjectKernel = new StandardKernel();ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 5M);IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>();ShoppingCart cart = new ShoppingCart(calcImpl);Console.WriteLine("Total: {0:c}", cart.CalculateStockValue());Console.ReadKey();
}

只是在Bind和To方法后添加了一个WithPropertyValue方法,其他代码都不用变,再一次见证了用Ninject编码的高效。
WithPropertyValue方法接收了两个参数,一个是属性名(示例中的"DiscountSize"),一个是属性值(示例中的5)。运行结果如下:

如果要给多个属性赋值,则可以在Bind和To方式后添加多个WithPropertyValue(<属性名>,<属性值>)方法。

我们还可以在类的实例化的时候为类的构造函数传递参数。为了演示,我们再把DefaultDiscountHelper类改一下:

public class DefaultDiscountHelper : IDiscountHelper { private decimal discountRate; public DefaultDiscountHelper(decimal discountParam) { discountRate = discountParam; } public decimal ApplyDiscount(decimal totalParam) { return (totalParam - (discountRate/ 10m * totalParam)); } 
}

显然,DefaultDiscountHelper类在实例化的时候必须给构造函数传递一个参数,不然程序会出错。和给属性赋值类似,只是用的方法是WithConstructorArgument(<参数名>,<参数值>),绑定方式如下代码所示:

...
ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); 
ninjectKernel.Bind<IDiscountHelper>() .To< DefaultDiscountHelper>().WithConstructorArgument("discountParam", 5M);
...

同样,只需要更改一行代码,其他代码原来怎么写还是怎么写。如果构造函数有多个参数,则需在Bind和To方法后面加上多个WithConstructorArgument即可。

3.自我绑定

Niject的一个非常好用的特性就是自绑定。当通过Bind和To方法绑定好接口和类后,可以直接通过ninjectKernel.Get<类名>()来获得一个类的实例。

在前面的几个示例中,我们都是像下面这样来创建ShoppingCart类实例的:

...
IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>();
ShoppingCart cart = new ShoppingCart(calcImpl);
...

其实有一种更简单的定法,如下:

... 
ShoppingCart cart = ninjectKernel.Get<ShoppingCart>(); 
... 

这种写法不需要关心ShoppingCart类依赖哪个接口,也不需要手动去获取该接口的实现(calcImpl)。当通过这句代码请求一个ShoppingCart类的实例的时候,Ninject会自动判断依赖关系,并为我们创建所需接口对应的实现。这种方式看起来有点怪,其实中规中矩的写法是:

...
ninjectKernel.Bind<ShoppingCart>().ToSelf();
ShoppingCart cart = ninjectKernel.Get<ShoppingCart>();
...

这里有自我绑定用的是ToSelf方法,在本示例中可以省略该句。但用ToSelf方法自我绑定的好处是可以在其后面用WithXXX方法指定构造函数参数、属性等等的值。

4.派生类绑定

通过一般绑定,当请求一个接口的实现时,Ninject会帮我们自动创建实现接口的类的实例。我们说某某类实现某某接口,也可以说某某类继承某某接口。如果我们把接口当作一个父类,是不是也可以把父类绑定到一个继承自该父类的子类呢?我们来实验一把。先改造一下ShoppingCart类,给它的CalculateStockValue方法改成虚方法:

public class ShoppingCart {protected IValueCalculator calculator;protected Product[] products;//构造函数,参数为实现了IEmailSender接口的类的实例public ShoppingCart(IValueCalculator calcParam) {calculator = calcParam;products = new[]{ new Product {Name = "西瓜", Category = "水果", Price = 2.3M}, new Product {Name = "苹果", Category = "水果", Price = 4.9M}, new Product {Name = "空心菜", Category = "蔬菜", Price = 2.2M}, new Product {Name = "地瓜", Category = "蔬菜", Price = 1.9M} };}//计算购物车内商品总价钱public virtual decimal CalculateStockValue() {//计算商品总价钱 decimal totalValue = calculator.ValueProducts(products);return totalValue;}
}

再添加一个ShoppingCart类的子类:

public class LimitShoppingCart : ShoppingCart {public LimitShoppingCart(IValueCalculator calcParam): base(calcParam) {}public override decimal CalculateStockValue() {//过滤价格超过了上限的商品var filteredProducts = products.Where(e => e.Price < ItemLimit);
return calculator.ValueProducts(filteredProducts.ToArray());}public decimal ItemLimit { get; set; } }

然后把父类ShoppingCart绑定到子类LimitShoppingCart:

public static void Main() {IKernel ninjectKernel = new StandardKernel();ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 5M);//派生类绑定ninjectKernel.Bind<ShoppingCart>().To<LimitShoppingCart>().WithPropertyValue("ItemLimit", 3M);ShoppingCart cart = ninjectKernel.Get<ShoppingCart>();Console.WriteLine("Total: {0:c}", cart.CalculateStockValue());Console.ReadKey();
}

运行结果:

从运行结果可以看出,cart对象调用的是子类的CalculateStockValue方法,证明了可以把父类绑定到一个继承自该父类的子类。通过派生类绑定,当我们请求父类的时候,Ninject自动帮我们创建一个对应的子类的实例,并将其返回。由于抽象类不能被实例化,所以派生类绑定在使用抽象类的时候非常有用。

5.条件绑定

当一个接口有多个实现或一个类有多个子类的时候,我们可以通过条件绑定来指定使用哪一个实现或子类。为了演示,我们给IValueCalculator接口再添加一个实现,如下:

public class IterativeValueCalculator : IValueCalculator { public decimal ValueProducts(params Product[] products) { decimal totalValue = 0; foreach (Product p in products) { totalValue += p.Price; } return totalValue; } 
}

IValueCalculator接口现在有两个实现:IterativeValueCalculator和LinqValueCalculator。我们可以指定,如果是把该接口的实现注入到LimitShoppingCart类,那么就用IterativeValueCalculator,其他情况都用LinqValueCalculator。如下所示:

public static void Main() {IKernel ninjectKernel = new StandardKernel();ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 5M);//派生类绑定ninjectKernel.Bind<ShoppingCart>().To<LimitShoppingCart>().WithPropertyValue("ItemLimit", 3M);//条件绑定ninjectKernel.Bind<IValueCalculator>().To<IterativeValueCalculator>().WhenInjectedInto<LimitShoppingCart>();ShoppingCart cart = ninjectKernel.Get<ShoppingCart>();Console.WriteLine("Total: {0:c}", cart.CalculateStockValue());Console.ReadKey();
}

运行结果:

运行结果是6.4,说明没有打折,即调用的是计算方法是IterativeValueCalculator的ValueProducts方法。可见,Ninject会查找最匹配的绑定,如果没有找到条件绑定,则使用默认绑定。在条件绑定中,除了WhenInjectedInto方法,还有When和WhenClassHas等方法,朋友们可以在使用的时候再慢慢研究。

在ASP.NET MVC中使用Ninject

本文用控制台应用程序演示了Ninject的使用,但要把Ninject集成到ASP.NET MVC中还是有点复杂的。首先要做的事就是创建一个继承System.Web.Mvc.DefaultControllerFactory的类,MVC默认使用这个类来创建Controller类的实例(后续博文会专门讲这个)。代码如下:

using System;
using Ninject;
using System.Web.Mvc;
using System.Web.Routing;namespace MvcApplication1 {public class NinjectControllerFactory : DefaultControllerFactory {private IKernel ninjectKernel;public NinjectControllerFactory() {ninjectKernel = new StandardKernel();AddBindings();}protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) {return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType);}private void AddBindings() {// 在这添加绑定,// 如:ninjectKernel.Bind<IProductRepository>().To<FakeProductRepository>();
        }}
}
NinjectControllerFactory

现在暂时不解释这段代码,大家都看懂就看,看不懂就过,只要知道在ASP.NET MVC中使用Ninject要做这么一件事就行。

添加完这个类后,还要做一件事,就是在MVC框架中注册这个类。一般我们在Global.asax文件中的Application_Start方法中进行注册,如下所示:

protected void Application_Start() {AreaRegistration.RegisterAllAreas();WebApiConfig.Register(GlobalConfiguration.Configuration);FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);RouteConfig.RegisterRoutes(RouteTable.Routes);ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());
}

注册后,MVC框架就会用NinjectControllerFactory类去获取Cotroller类的实例。在后续博文中会具体演示如何在ASP.NET MVC中使用Ninject,这里就不具体演示了,大家知道需要做这么两件事就行。

虽然我们前面花了很大功夫来学习Ninject就是为了在MVC中使用这样一个NinjectControllerFactory类,但是了解Ninject如何工作是非常有必要的。理解好了一种DI容器,可以使得开发和测试更简单、更高效。

 

参考:

《Pro ASP.NET MVC 3 Framework》

转载于:https://www.cnblogs.com/willick/p/3223042.html


http://www.ppmy.cn/news/111384.html

相关文章

[ASP.NET MVC 小牛之路]06 - 使用 Entity Framework

在家闲着也是闲着&#xff0c;继续写我的[ASP.NET MVC 小牛之路]系列吧。在该系列的上一篇博文中&#xff0c;在显示书本信息列表的时候&#xff0c;我们是在程序代码中手工造的数据。本文将演示如何在ASP.NET MVC中使用Entity Framework从数据库中获取数据。虽然本文题目听上去…

小牛电动车的亮点与槽点分析

转载自“来自智能硬件网” 作者&#xff1a;金楠 本文地址&#xff1a;http://www.znyj365.com/6132.htm 前途光明&#xff0c;现实还堵着&#xff0c;小牛电动车的亮点与槽点分析 金楠 发表于 : 2015-06-03 评论 (6) 浏览 (2001) 前天下午&#xff0c;牛电科技在北京798…

做“小米”还是“特斯拉”,后疫情时代小牛电动如何走?

图片来源于网络 文|陈小江 来源 | 螳螂财经&#xff08;ID:TanglangFin&#xff09; 北京时间8月17日&#xff0c;小牛电动发布了截至2020年6月30日的第二季度未经审计财报。财报显示&#xff0c;小牛电动二季度营收6.45亿元&#xff0c;同比增长21.6%&#xff1b;二季度净利…

锂电紧缺?小牛电动宣布涨价,爱玛科技、新日股份未有动作

3月21日&#xff0c;小牛电动&#xff08;NASDAQ:NIU&#xff09;发布公告称&#xff0c;“受上游锂电等原材料&#xff08;价格&#xff09;大幅上涨&#xff0c;小牛电动将于2022年4月1日对全系锂电产品零售指导价进行一次上调&#xff0c;上调金额200-1000元不等。具体以届时…

2倍研发费用=营销费用,小牛电动“智”在何方?

因全球气候变化形势严峻&#xff0c;能源及空气质量每况愈下&#xff0c;交通电气化逐渐成为大势所趋。 新能源的迅猛发展颠覆了传统车企行业&#xff0c;除了四轮汽车外&#xff0c;对两轮电动车领域也带来了较大影响。 2014年&#xff0c;小牛电动凭借“智能化锂电化”打入…

降价促销增收不增利,小牛电动能否借智能化战略顺利上位?

一直以来&#xff0c;一季度都是电动自行车销售的淡季。国内几大电动自行车&#xff0c;雅迪、爱玛也好&#xff0c;小牛电动也好&#xff0c;在一季度销量最为疲软&#xff0c;但今年的小牛电动好像和以往不太一样。 北京时间5月17日&#xff0c;小牛电动发布了第一季度财报&…

当小牛正面杠上爱玛雅迪,二轮电车市场中谁主沉浮?

2021年10月11日&#xff0c;小牛电动公布的第三季度销售数据中显示&#xff0c;第三季度两轮电动车销量持续增长&#xff0c;售出397079辆&#xff0c;同比增长58.3%&#xff1b;另外&#xff0c;第三季度中小牛继续拓展零售网络覆盖范围&#xff0c;截至9月底小牛在国内的门店…

加湿助眠仪语音IC芯片 白噪音语音方案 WTN6040F-8S

近年来&#xff0c;随着人们健康意识的不断增强&#xff0c;助眠仪逐渐成为了一种备受欢迎的家居健康设备。随着科技的不断升级&#xff0c;助眠仪也在不断地进行改进&#xff0c;以满足用户需求。其中&#xff0c;一种值得注意的改进就是助眠仪音乐播报芯片的应用。加湿助眠仪…

小牛市启示录:有价值何必等风来

自四月初以来&#xff0c;比特币就开启了“起飞模式”&#xff1a;从4100美金左右&#xff0c;一路狂飙&#xff0c;经过三周的拉升&#xff0c;一度触及最高5600美金&#xff0c;涨幅达到36.6%&#xff0c;目前仍在5300美金徘徊&#xff0c;虽有所回调&#xff0c;但从几轮的压…

[ASP.NET MVC 小牛之路]05 - 使用 Ninject

在[ASP.NET MVC 小牛之路]系列上一篇文章&#xff08;依赖注入(DI)和Ninject&#xff09;的末尾提到了在ASP.NET MVC中使用Ninject要做的两件事情&#xff0c;续这篇文章之后&#xff0c;本文将用一个实际的示例来演示Ninject在ASP.NET MVC中的应用。 为了更好的理解和撑握本文…

对话小牛电动CEO李彦:我们要做有独特价值主张的产品

雷递网 雷建平 8月5日报道 小牛电动日前推出两款新品——全场景跨界机能座驾SQi及开挂电自全新UQi2022版&#xff0c;开启两轮电自“个性化”新时代。 据介绍&#xff0c;小牛电动2015年将智能和锂电应用于两轮电动车&#xff0c;此后&#xff0c;开启两轮电自2.0时代。截至目前…

小牛电动公布IPO后首份财报:净亏220万 大幅收窄

雷帝网 雷建平 11月20日报道 小牛电动&#xff08;NASDAQ:NIU&#xff09;今天公布财报&#xff0c;财报显示&#xff0c;小牛电动第三季度净营收4.932亿元&#xff0c;与上年同期相比增长86.1%&#xff1b; 小牛电动第三季度净亏损为220万元&#xff0c;与上年同期的净亏损393…

亏损还去美国上市?小牛电动没有那么简单

雷帝网 雷建平 10月18日报道 2018年赴美上市大潮再迎一员&#xff0c;牛电科技即将在美国纳斯达克IPO。 近日&#xff0c;牛电科技向美国证券交易委员会提交招股说明书&#xff0c;根据10月16日最新招股书显示&#xff0c;牛电科技将在IPO中发行830万份ADS。 牛电科技价格区间在…

[ASP.NET MVC 小牛之路]02 - C#知识点提要

特别提醒&#xff1a;本文编写时间是 2013 年&#xff0c;请根据目前 .NET 发展接收你所需的知识点。 本篇博文主要对asp.net mvc开发需要撑握的C#语言知识点进行简单回顾&#xff0c;尤其是C# 3.0才有的一些C#语言特性。对于正在学asp.net mvc的童鞋&#xff0c;不防花个几分钟…

小牛电动车之我见

小牛推都是锂电版&#xff0c;价格为3999和4999两款。 个人认为&#xff0c;贵了&#xff1b;如果上下班用&#xff0c;用铅酸电池足够。 所以&#xff0c;李总应该推一款2499的入门版本&#xff0c;用铅酸电池下降成本&#xff1b;同时可以用户置换成锂电池&#xff0c;当用户…

百变五小牛开发历程

# 欢迎使用Markdown编辑器写博客 本Markdown编辑器使用StackEdit修改而来&#xff0c;用它写博客&#xff0c;将会带来全新的体验哦&#xff1a; Markdown和扩展Markdown简洁的语法代码块高亮图片链接和图片上传LaTex数学公式UML序列图和流程图离线写博客导入导出Markdown文件…

git (本地仓库)和(远程仓库)之间的代码推送:013

这里先说明一下循序&#xff1a; 1. 创建(远程仓库)和(本地仓库) 2. 创建(远程仓库)和(本地仓库)之间的链接 3. 将(本地仓库)的代码推通过命令送到(远程仓库)&#xff1b;将(本地仓库)的代码通过(TortoiseGit小乌龟)推送到(远程仓库) 1. 创建(远程仓库)和(本地仓库)&#xff0c…

【软件质量与软件测试 白盒测试与黑盒测试】

第十章 黑盒测试 10.1 等价类划分&#xff1a; 10.1.1 划分等价类 等价类是指所有数据中的一组&#xff0c;它们具有相同的测试结果或相同的响应。等价类划分是将输入数据分为多个等价类的过程。 10.1.2 划分等价类的方法 划分等价类方法主要包括以下几种&#xff1a; 特…

win10没有hyper-v功能解决方法

1.在Win10搜索框,搜索PowerShell 2.打开 Windows PowerShell&#xff0c;输入 systeminfo 命令 如果四个全为 “是”&#xff0c;则表示支持 Hyper-V 功能 3.桌面新建一个记事本文件&#xff0c;将它的后缀改成bat&#xff0c;复制下面的代码 pushd "%~dp0"dir /b …

计算机组成原理 之 第四章 指令系统

1. 指令格式 通常包括操作码字段&#xff08;OP&#xff09;和地址码字段&#xff08;A&#xff09;&#xff0c;有的指令不需要地址码 指令系统&#xff08;指令集&#xff09;&#xff1a;一台计算机的所有指令的集合&#xff0c;eg&#xff1a;X86、ARM &#xff08;1&#…
最新文章