前言

对于IOC和DI,可能每个人都能说出自己的理解。IOC全称是Inversion of Control翻译成中文叫控制反转,简单的说就是把对象的控制权反转到IOC容器中,由IOC管理其生命周期。DI全称是DependencyInjection翻译成中文叫依赖注入,就是IOC容器把你依赖的模块通过注入的方式提供给你,而不是你自己主动去获取,其形式主要分为构造注入和属性注入,Core自带的DI只支持构造注入,至于为什么,最多的说法就是构造注入能使得依赖变得更清晰,我既然依赖你,那么我实例化的时候你就必须得出现。而构造函数恰恰就承担着这种责任。

简单介绍

很多人接触它的时候应该都是从Asp.Net Core学习过程中开始的。其实它本身对Asp.Net Core并无依赖关系,Asp.Net Core依赖DI,但是这套框架本身并不只是可以提供给Asp.Net Core使用,它是一套独立的框架,开源在微软官方Github的extensions仓库中具体地址是https://github.com/dotnet/extensions/tree/v3.1.5/src/DependencyInjection。关于如何使用,这里就不再多说了,相信大家都非常清楚了。那咱们就说点不一样的。

服务注册

我们都知道提供注册的服务名称叫IServiceCollection,我们大部分情况下主要使用它的AddScoped、AddTransient、AddSingleton来完成注册。我们就先查看一下IServiceCollection接口的具体实现,找到源码位置

public interface IServiceCollection : IList<ServiceDescriptor>
{
}

(⊙o⊙)…额,你并没有看错,这次我真没少贴代码,其实IServiceCollection本质就是IList,而且并没有发现AddScoped、AddTransient、AddSingleton踪影,说明这几个方法是扩展方法,我们找到ServiceCollectionServiceExtensions扩展类的位置,我们平时用的方法都在这里,由于代码非常多这里就不全部粘贴出来了,我们只粘贴AddTransient相关的,AddScoped、AddSingleton的实现同理

/// <summary>
/// 通过泛型注册
/// </summary>
public static IServiceCollection AddTransient<TService, TImplementation>(this IServiceCollection services)where TService : classwhere TImplementation : class, TService
{if (services == null){throw new ArgumentNullException(nameof(services));}//得到泛型类型return services.AddTransient(typeof(TService), typeof(TImplementation));
}/// <summary>
/// 根据类型注册
/// </summary>
public static IServiceCollection AddTransient(this IServiceCollection services,Type serviceType,Type implementationType)
{if (services == null){throw new ArgumentNullException(nameof(services));}if (serviceType == null){throw new ArgumentNullException(nameof(serviceType));}if (implementationType == null){throw new ArgumentNullException(nameof(implementationType));}return Add(services, serviceType, implementationType, ServiceLifetime.Transient);
}/// <summary>
/// 根据类型实例来自工厂注册方法
/// </summary>
public static IServiceCollection AddTransient(this IServiceCollection services,Type serviceType,Func<IServiceProvider, object> implementationFactory)
{if (services == null){throw new ArgumentNullException(nameof(services));}if (serviceType == null){throw new ArgumentNullException(nameof(serviceType));}if (implementationFactory == null){throw new ArgumentNullException(nameof(implementationFactory));}return Add(services, serviceType, implementationFactory, ServiceLifetime.Transient);
}

通过以上代码我们可以得到两个结论,一是注册服务的方法本质都是在调用Add重载的两个方法,二是声明周期最终还是通过ServiceLifetime来控制的AddScoped、AddTransient、AddSingleton只是分文别类的进行封装而已,我们来看ServiceLifetime的源码实现

public enum ServiceLifetime
{/// <summary>/// 指定将创建服务的单个实例。/// </summary>Singleton,/// <summary>/// 指定每个作用域创建服务的新实例。/// </summary>Scoped,/// <summary>/// 指定每次请求服务时都将创建该服务的新实例。/// </summary>Transient
}

这个枚举是为了枚举我们注册服务实例的声明周期的,非常清晰不在过多讲述,接下来我们看核心的两个Add方法的实现

private static IServiceCollection Add(IServiceCollection collection,Type serviceType,Type implementationType,ServiceLifetime lifetime)
{var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime);collection.Add(descriptor);return collection;
}private static IServiceCollection Add(IServiceCollection collection,Type serviceType,Func<IServiceProvider, object> implementationFactory,ServiceLifetime lifetime)
{var descriptor = new ServiceDescriptor(serviceType, implementationFactory, lifetime);collection.Add(descriptor);return collection;
}

通过这两个核心方法我们可以非常清晰的了解到注册的本质其实就是构建ServiceDescriptor实例然后添加到IServiceCollection即IList中,这里我们都是列举的根据实例去注册抽象的类型,还有一种是只注册具体类型或者具体实例的方法,这个是怎么实现的呢。

public static IServiceCollection AddTransient(this IServiceCollection services,Type serviceType)
{if (services == null){throw new ArgumentNullException(nameof(services));}if (serviceType == null){throw new ArgumentNullException(nameof(serviceType));}//把自己注册给自己return services.AddTransient(serviceType, serviceType);
}

通过这个方法我们就可以看到其实注册单类型的方法,也是通过调用的注入实例到抽象的方法,只不过是将自己注册给了自己。
好了,抽象和扩展方法我们就先说到这里,接下来我们来看IServiceCollection的实现类ServiceCollection的实现

public class ServiceCollection : IServiceCollection
{private readonly List<ServiceDescriptor> _descriptors = new List<ServiceDescriptor>();public int Count => _descriptors.Count;public bool IsReadOnly => false;public ServiceDescriptor this[int index]{get{return _descriptors[index];}set{_descriptors[index] = value;}}public void Clear(){_descriptors.Clear();}public bool Contains(ServiceDescriptor item){return _descriptors.Contains(item);}public void CopyTo(ServiceDescriptor[] array, int arrayIndex){_descriptors.CopyTo(array, arrayIndex);}public bool Remove(ServiceDescriptor item){return _descriptors.Remove(item);}public IEnumerator<ServiceDescriptor> GetEnumerator(){return _descriptors.GetEnumerator();}void ICollection<ServiceDescriptor>.Add(ServiceDescriptor item){_descriptors.Add(item);}IEnumerator IEnumerable.GetEnumerator(){return GetEnumerator();}public int IndexOf(ServiceDescriptor item){return _descriptors.IndexOf(item);}public void Insert(int index, ServiceDescriptor item){_descriptors.Insert(index, item);}public void RemoveAt(int index){_descriptors.RemoveAt(index);}
}

这个类就非常清晰,也非常简单了。ServiceCollection承载了一个List的集合,由于实现了IList接口,所以该类实现了接口的方法,实现了对List集合的操作,其核心就是ServiceDescriptor服务描述类,我们看一下大致的源码。

public class ServiceDescriptor
{public ServiceDescriptor(Type serviceType,Type implementationType,ServiceLifetime lifetime): this(serviceType, lifetime){ImplementationType = implementationType;}public ServiceDescriptor(Type serviceType,object instance): this(serviceType, ServiceLifetime.Singleton){ImplementationInstance = instance;}public ServiceDescriptor(Type serviceType,Func<IServiceProvider, object> factory,ServiceLifetime lifetime): this(serviceType, lifetime){ImplementationFactory = factory;}private ServiceDescriptor(Type serviceType, ServiceLifetime lifetime){Lifetime = lifetime;ServiceType = serviceType;}public ServiceLifetime Lifetime { get; }public Type ServiceType { get; }public Type ImplementationType { get; }public object ImplementationInstance { get; }public Func<IServiceProvider, object> ImplementationFactory { get; }
}

这里我们只是粘贴了初始化的方法,通过这个初始化我们得到了,本质其实就是给描述具体注册的Lifetime、ServiceType、ImplementationType、ImplementationInstance、ImplementationFactory赋值。在平时的使用中,我们在注册服务的时候还会用到这种注册方式

services.Add(ServiceDescriptor.Scoped<IPersonService, PersonService>());
//services.Add(ServiceDescriptor.Scoped(typeof(IPersonService),typeof(PersonService)));
//或
services.Add(ServiceDescriptor.Transient<IPersonService, PersonService>());
//services.Add(ServiceDescriptor.Transient(typeof(IPersonService), typeof(PersonService)));
//或
services.Add(ServiceDescriptor.Singleton<IPersonService, PersonService>());
//services.Add(ServiceDescriptor.Singleton(typeof(IPersonService), typeof(PersonService)));

这种注册方式是通过ServiceDescriptor自身的操作去注册相关实例,我们拿出来其中一个Transient看一下具体实现

public static ServiceDescriptor Transient<TService, TImplementation>()where TService : classwhere TImplementation : class, TService
{//都是在调用Describereturn Describe<TService, TImplementation>(ServiceLifetime.Transient);
}public static ServiceDescriptor Transient(Type service, Type implementationType)
{//都是在调用Describereturn Describe(service, implementationType, ServiceLifetime.Transient);
}public static ServiceDescriptor Describe(Type serviceType, Type implementationType, ServiceLifetime lifetime)
{//还是返回ServiceDescriptor实例return new ServiceDescriptor(serviceType, implementationType, lifetime);
}public static ServiceDescriptor Describe(Type serviceType, Func<IServiceProvider, object> implementationFactory, ServiceLifetime lifetime)
{//还是返回ServiceDescriptor实例return new ServiceDescriptor(serviceType, implementationFactory, lifetime);
}

通过这个我们就可以了解到ServiceDescriptor.Scoped、ServiceDescriptor.Singleton、ServiceDescriptor.Singleton其实是调用的Describe方法,Describe的本身还是去实例化ServiceDescriptor,殊途同归,只是多了种写法,最终还是去构建ServiceDescriptor。通过这么多源码的分析得出的结论就一点IServiceCollection注册的本质就是在构建ServiceDescriptor集合。

服务提供

上面我们了解到了服务注册相关,至于服务是怎么提供出来的,大家应该都是非常熟悉了其实是根据IServiceCollection构建出来的

IServiceProvider serviceProvider = services.BuildServiceProvider();

BuildServiceProvider并不是IServiceCollection的自带方法,所以也是来自扩展方法,找到ServiceCollectionContainerBuilderExtensions扩展类,最终都是在执行这个方法

public static ServiceProvider BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options)
{return new ServiceProvider(services, options);
}

BuildServiceProvider的时候需要传递ServiceProviderOptions这个类主要是配置是否校验作用域和提供的实例来自于那种提供引擎使用

public class ServiceProviderOptions
{internal static readonly ServiceProviderOptions Default = new ServiceProviderOptions();/// <summary>/// 是够在编译的时候校验作用域范围检查/// </summary>public bool ValidateScopes { get; set; }/// <summary>/// 是够在编译的时候校验作用域范围检查/// </summary>public bool ValidateOnBuild { get; set; }/// <summary>/// 配置使用那种方式提供ServiceProvider的承载的具体实例/// </summary>internal ServiceProviderMode Mode { get; set; } = ServiceProviderMode.Default;
}internal enum ServiceProviderMode
{Default,Dynamic,Runtime,Expressions,ILEmit
}

作用域范围检查还是非常严格的,不开启的也会有一定的依赖规则,简单总结一下

  • 如果开启了范围检查,有依赖关系的模型如果生命周期不一致就会报错,如果不存Scope声明但是获取AddScoped也是会有异常的

  • 如果不开启范围检查,如果生命周期长的依赖生命周期短的,那么被依赖的模型将会被提升和依赖模型同等的生命周期。如果生命周期短的模型依赖生命周期长的模型,将保持和注册时候的生命周期一致。

接下来我们查看一下服务提供核心IServiceProvider的实现,这个接口只包含一个抽象,那就是根据"注册类型"获取具体实例,其他获取实例的方法都是根据这个方法扩展而来

public interface IServiceProvider
{object GetService (Type serviceType);
}

ServiceProvider是IServiceProvider的默认实现类,它是获取注册实例的默认出口类,我们只看提供服务相关的

public sealed class ServiceProvider : IServiceProvider, IDisposable, IServiceProviderEngineCallback, IAsyncDisposable
{private readonly IServiceProviderEngine _engine;private readonly CallSiteValidator _callSiteValidator;internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options){IServiceProviderEngineCallback callback = null;if (options.ValidateScopes){callback = this;_callSiteValidator = new CallSiteValidator();}//根据ServiceProviderMode的值判断才有那种方式去实例化对象switch (options.Mode){//默认方式case ServiceProviderMode.Default:if (RuntimeFeature.IsSupported("IsDynamicCodeCompiled")){_engine = new DynamicServiceProviderEngine(serviceDescriptors, callback);}else{_engine = new RuntimeServiceProviderEngine(serviceDescriptors, callback);}break;case ServiceProviderMode.Dynamic:_engine = new DynamicServiceProviderEngine(serviceDescriptors, callback);break;case ServiceProviderMode.Runtime:_engine = new RuntimeServiceProviderEngine(serviceDescriptors, callback);break;//if IL_EMITcase ServiceProviderMode.ILEmit:_engine = new ILEmitServiceProviderEngine(serviceDescriptors, callback);break;case ServiceProviderMode.Expressions:_engine = new ExpressionsServiceProviderEngine(serviceDescriptors, callback);break;default:throw new NotSupportedException(nameof(options.Mode));}//判断是否开启编译时范围校验if (options.ValidateOnBuild){List<Exception> exceptions = null;foreach (var serviceDescriptor in serviceDescriptors){try{_engine.ValidateService(serviceDescriptor);}catch (Exception e){}}}}/// <summary>/// 通过IServiceProviderEngine获取具体实例的方法/// </summary>public object GetService(Type serviceType) => _engine.GetService(serviceType);
}

在这个类里,关于提供具体实例的操作还是非常清晰的,关于更深的IServiceProviderEngine这里就不过多介绍了,有兴趣的可以自行在GitHub上查阅。

关于Scope问题

在声明周期里Scope是比较特殊也是比较抽象的一个,我们使用的时候是通过当前serviceProvider创建子作用域

using (IServiceScope scope = serviceProvider.CreateScope())
{IServiceProvider scopeProvider = scope.ServiceProvider;
}

它大概的思路就是在当前容器中创建一个作用域,scope.ServiceProvider来获取这个子容器作用域里的实例。Singleton类型的实例直接去根容器获取,所以和当前子容器作用域无关。Scoped类型的实例,在当前作用域内唯一,无论获取多少次返回的都是同一个实例。Transient类型的只要去获取都是返回新的实例。当前IServiceScope释放的时候Scoped类型的实例也会被释放,注意!!!Transient类型的实例也是在当前IServiceScope Dispose的时候去释放,尽管你每次获取的时候都是新的实例,但是释放的时候都是统一释放的。在当前ServiceScope内你可以继续创建当前Scope的IServiceScope。其实通过这里也不难发现根容器的Scoped其实就是等同于Singleton,其生命周期都是和应用程序保持一致。
    Scope问题在如果写控制台之类的程序其作用可能不是很明显,除非有特殊的要求,在Asp.Net Core中使用还是比较深入的。Asp.Net Core在启动的时候会创建serviceProvider,这个serviceProvider的Scope是跟随程序的生命周期一致的,它是作为所有服务实例的根容器。在Asp.Net Core中有几种情况的实例和请求无关也就是说在程序运行期间是单例情况的,我们使用的时候需要注意的地方

  • 通过Startup.cs的构造函数注入的IHostEnvironment、IWebHostEnvironment、IConfiguration

  • 在Startup.cs类中的Configure方法注入的

  • 使用约定方式自定义的中间件,是在程序初始化的时候被执行的所以根据约定方式定义的中间件的构造函数注入的也是单例的。

  • 使用约定方式自定义的中间件,是在程序初始化的时候被执行的所以根据约定方式定义的中间件的构造函数注入的也是单例的。

其实就一点,在程序初始化过程中创建的类大部分都是和请求无关的,通常这一类方法或者具体的实例注入的依赖都是和程序生命周期保持一致的,即单例模式。Asp.Net Core在每次处理请求的时候会在根容器创建一个Scope范围的ServiceProvider,也就是我们所说的Asp.Net Core在每次请求过程中是唯一的情况。

  • 自定义实现了IMiddleware的中间件,且生命周期为Scoped的情况。

  • 中间件中Invoke或InvokeAsync注入的相关实例,且注册的时候为Scoped的情况。

  • Controller中或者为Controller提供服务的相关类,比如EF SQLConnection或其他连接服务相关,或者自定义的Service等,且注册的时候为Scoped的情况。 这里说明一点,默认情况下Controller并不是通过容器创建的,而是通过反射创建的。如果需要将Controller也托管到容器中,需要使用services.AddControllers().AddControllersAsServices()的方式,这个操作在使用Autofac容器的时候在Controller中使用属性注入是必不可少的。

  • 还有就是通过Inject注册到RazorPage视图页面中的情况。

关于UseServiceProviderFactory

UseServiceProviderFactory方法主要是为我们提供了替换默认容器的操作,通过这个方法可以将三方的IOC框架结合进来比如Autofac。我们可以查看UseServiceProviderFactory具体的实现,了解它的工作方式。这个方法来自HostBuilder类

public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory)
{_serviceProviderFactory = new ServiceFactoryAdapter<TContainerBuilder>(factory ?? throw new ArgumentNullException(nameof(factory)));return this;
}

我们找到_serviceProviderFactory定义的地方,默认值就是为ServiceFactoryAdapter传递了DefaultServiceProviderFactory实例。

private IServiceFactoryAdapter _serviceProviderFactory = new ServiceFactoryAdapter<IServiceCollection>(new DefaultServiceProviderFactory());

继续查找ServiceFactoryAdapter的大致核心实现

internal class ServiceFactoryAdapter<TContainerBuilder> : IServiceFactoryAdapter
{private IServiceProviderFactory<TContainerBuilder> _serviceProviderFactory;public object CreateBuilder(IServiceCollection services){return _serviceProviderFactory.CreateBuilder(services);}public IServiceProvider CreateServiceProvider(object containerBuilder){return _serviceProviderFactory.CreateServiceProvider((TContainerBuilder)containerBuilder);}
}

通过查找HostBuilder中这段源码我们可以知道ServiceFactoryAdapter创建出来的容器是供整个Host使用的。也就是说我们在程序中使用的容器相关的都是由它提供的。
接下来我们看下默认的DefaultServiceProviderFactory的大致实现。找到源码位置

public class DefaultServiceProviderFactory : IServiceProviderFactory<IServiceCollection>
{public IServiceCollection CreateBuilder(IServiceCollection services){return services;}public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder){return containerBuilder.BuildServiceProvider(_options);}
}

没啥逻辑,其实就是把默认的IServiceCollection和IServiceProvider通过工厂的形式提供出来。这么做的目的只有一个,就是降低依赖的耦合度方便我们能够介入第三方的IOC框架。口说无凭,接下来我们就看一下Autofac是怎么适配进来的。我们在GitHub上找到Autofac.Extensions.DependencyInjection仓库的位置https://github.com/autofac/Autofac.Extensions.DependencyInjection,找到Autofac中IServiceProviderFactory实现类AutofacServiceProviderFactory,看看他是如何适配到默认的IOC框架的

public class AutofacServiceProviderFactory : IServiceProviderFactory<ContainerBuilder>
{private readonly Action<ContainerBuilder> _configurationAction;public AutofacServiceProviderFactory(Action<ContainerBuilder> configurationAction = null){_configurationAction = configurationAction ?? (builder => { });}public ContainerBuilder CreateBuilder(IServiceCollection services){//由于是使用Autofac本身的容器去工作,所以返回的Autofac承载类ContainerBuildervar builder = new ContainerBuilder();//将现有的IServiceCollection中注册的实例托管到ContainerBuilder中builder.Populate(services);//这一步是我们自定义注入到Autofac方法的委托,及我们在Startup类中定义的//public void ConfigureContainer(ContainerBuilder builder)方法_configurationAction(builder);return builder;}public IServiceProvider CreateServiceProvider(ContainerBuilder containerBuilder){if (containerBuilder == null) throw new ArgumentNullException(nameof(containerBuilder));//获取Container容器,因为接下来要使用获取实例的方法了var container = containerBuilder.Build();//这个类实现了IServiceProvider接口//实现了public object GetService(Type serviceType)方法从Autofac的Container中获取实例return new AutofacServiceProvider(container);}
}

IServiceProviderFactory的工作其实就是适配符合我们使用的适配器模式,其核心就是用你的容器去托管注册到IServiceCollection中的服务。然后用你的容器去构建IServiceProvider实例。

总结

通过以上我们对自带的DependencyInjection工作方式有了一定的了解,而且其扩展性非常强,能够使我们通过自己的方式去构建服务注册和注入,我们以Autofac为例讲解了三方容器集成到自带IOC的方式。有很多核心的源码并没有讲解到,因为怕自己理解不够,就不误导大家了。我在上文中涉及到源码的地方基本上都加了源码的连接,可以直接点进去查看源码,之前源码探究相关的文章也都是一样,可能之前有许多同学没有注意到。主要原因是我粘贴出来的代码有删减,最重要的还是怕自己理解不到位,误导了大家,这样就能用过点击自己查看源码了。如有你有更好的理解,或者觉得我讲解的理解不到的地方,欢迎评论区沟通交流。

????欢迎扫码关注我的公众号????

浅谈.Net Core DependencyInjection源码探究相关推荐

  1. Matlab GUI/APP 浅谈(附计算器源码)

    Matlab GUI/APP 浅谈(附计算器源码) 今天没有什么段子,也没有心灵鸡汤.毒鸡汤啥的,纯粹聊一聊这些年从有关MATLAB GUI/APP开发中悟出的一点道理,顺便把计算器的源代码给大家. ...

  2. .NET Core HttpClient源码探究

    前言 在之前的文章我们介绍过HttpClient相关的服务发现,确实HttpClient是目前.NET Core进行Http网络编程的的主要手段.在之前的介绍中也看到了,我们使用了一个很重要的抽象Ht ...

  3. .Net Core Configuration源码探究

    前言 上篇文章我们演示了为Configuration添加Etcd数据源,并且了解到为Configuration扩展自定义数据源还是非常简单的,核心就是把数据源的数据按照一定的规则读取到指定的字典里,这 ...

  4. linkedhashmap遍历_Java集合:浅谈LinkedHashMap、LinkedHashSet源码及LRU算法实现

    Java的HashSet.HashMap集合应用及底层原理,相信大家都已经很熟悉了,这里就不再赘述了.这里主要来介绍下如何Java中的LinkedHashMap集合,同时也介绍下基于LinkedHas ...

  5. 红黑树原理浅谈(附Linux内核源码注释)

    引言:红黑树(英语:Red–black tree)是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组.它是在1972年由鲁道夫·贝尔发明的,他称之为"对称二 ...

  6. MCU系列之-浅谈sbus通信协议(源码,原理图均有)

    大家好,今天我发表一篇关于sbus通信协议的解析,刚开始作者为啥要做这个东西呢,因为作者在2019年4月21日参加了广东举办的中国工程机器人大赛,做的是小型无人机, 作者用的是FS-i6遥控,刚开始的 ...

  7. Vue源码探究-全局API

    Vue源码探究-全局API 本篇代码位于vue/src/core/global-api/ Vue暴露了一些全局API来强化功能开发,API的使用示例官网上都有说明,无需多言.这里主要来看一下全局API ...

  8. ASP.NET Core 框架源码地址

    ASP.NET Core 框架源码地址 https://github.com/dotnet/corefx 这个是.net core的 开源项目地址 https://github.com/aspnet  ...

  9. Vue源码探究-事件系统

    Vue源码探究-事件系统 本篇代码位于vue/src/core/instance/events.js 紧跟着生命周期之后的就是继续初始化事件相关的属性和方法.整个事件系统的代码相对其他模块来说非常简短 ...

最新文章

  1. java stream 转 file_java 中 byte[]、File、InputStream 互相转换
  2. python类方法继承_对python中类的继承与方法重写介绍
  3. int arr 13 java,java学习13 - 数组的定义、操作、异常、二维数组
  4. 采用rsync实现两台solaris服务之间的文件同步
  5. python数学符号表示方法_用Python学数学之Sympy代数符号运算
  6. 机器人学导论—机器人相关术语
  7. 速锐得驾培驾考免接线OBD数据价值及发展思路
  8. CMMI3 和 CMMI 4
  9. 中国联通家庭网关破解管理员账户
  10. 12款华丽的Admin管理后台模板
  11. Python的Profile概述
  12. .join()用法 | python学习
  13. 在大学城开一间宾馆能挣多少钱?
  14. maya正交视图锁定与解锁
  15. seurat使用笔记(数据处理、PCA、聚类)
  16. Python——创建对象
  17. 研究生复试--中文自我介绍
  18. 转:hosts文件及修改hosts的作用
  19. Android属性动画实现TextView类似支付宝余额数字滚动
  20. 人机交互:虚拟翻书与空中翻书的种类与技术原理及案例展示

热门文章

  1. URAL 1682 Crazy Professor (并查集)
  2. ionic 中文 API CSS and javascript link
  3. TCP/IP 协议简单分析(建立连接握手过程)
  4. iPhone5:4G是否进入主流的风向标?
  5. sketch怎么移动图层_什么是Photoshop Express,Fix,Mix和Sketch移动应用程序?
  6. 计算机网络udp实验时间戳请求报文与应答报文的表格填写,自考计算机网络管理历年(2007.1-2013.1)试题及答案(标有页码)...
  7. matlab胡良剑第五章,MATLAB习题参考答案(胡良剑,孙晓君)
  8. C++回声服务器_4-UDP connect版本客户端
  9. 原生sql实现restful接口调用
  10. some demos