C# 9.0中引入的新特性init和record的使用思考
写在前面
.NET 5.0已经发布,C# 9.0也为我们带来了许多新特性,其中最让我印象深刻的就是init和record type,很多文章已经把这两个新特性讨论的差不多了,本文不再详细讨论,而是通过使用角度来思考这两个特性。
init
init是C# 9.0中引入的新的访问器,它允许被修饰的属性在对象初始化的时候被赋值,其他场景作为只读属性的存在。直接使用的话,可能感受不到init的意义,所以我们先看看之前是如何设置属性为只读的。
private set设置属性为只读
设置只读属性有很多种方式,本文基于private set来讨论。
首先声明一个产品类,如下代码所示,我们把Id设置成了只读,这个时候也就只能通过构造函数来赋值了。在通常情况下,实体的唯一标识是不可更改的,同时也要防止Id被意外更改。
1: public class Product
2: {
3: public Product(int id)
4: {
5: this.Id = id;
6: }
7:
8: public int Id { get; private set; }
9: //public int Id { get; }
10:
11: public string ProductName { get; set; }
12:
13: public string Description { get; set; }
14: }
15:
16: class Program
17: {
18: static void Main(string[] args)
19: {
20: Product product = new Product(1)
21: {
22: ProductName = "test001",
23: Description = "Just a description"
24: };
25:
26: Console.WriteLine($"Current Product Id: {product.Id},\n\rProduct Name: {product.ProductName}, \n\rProduct Description: {product.Description}");
27:
28: //运行结果
29: //Current Product Id: 1,
30: //Product Name: test001,
31: //Product Description: Just a description
32:
33: Console.ReadKey();
34: }
35: }
.csharpcode, .csharpcode pre { font-size: small; color: rgba(0, 0, 0, 1); font-family: consolas, "Courier New", courier, monospace; background-color: rgba(255, 255, 255, 1) }
.csharpcode pre { margin: 0 }
.csharpcode .rem { color: rgba(0, 128, 0, 1) }
.csharpcode .kwrd { color: rgba(0, 0, 255, 1) }
.csharpcode .str { color: rgba(0, 96, 128, 1) }
.csharpcode .op { color: rgba(0, 0, 192, 1) }
.csharpcode .preproc { color: rgba(204, 102, 51, 1) }
.csharpcode .asp { background-color: rgba(255, 255, 0, 1) }
.csharpcode .html { color: rgba(128, 0, 0, 1) }
.csharpcode .attr { color: rgba(255, 0, 0, 1) }
.csharpcode .alt { background-color: rgba(244, 244, 244, 1); width: 100%; margin: 0 }
.csharpcode .lnum { color: rgba(96, 96, 96, 1) }
record方式设置只读
使用init方式,是非常简单的,只需要把private set改成init就行了:
1: public int Id { get; init; }
.csharpcode, .csharpcode pre { font-size: small; color: rgba(0, 0, 0, 1); font-family: consolas, "Courier New", courier, monospace; background-color: rgba(255, 255, 255, 1) }
.csharpcode pre { margin: 0 }
.csharpcode .rem { color: rgba(0, 128, 0, 1) }
.csharpcode .kwrd { color: rgba(0, 0, 255, 1) }
.csharpcode .str { color: rgba(0, 96, 128, 1) }
.csharpcode .op { color: rgba(0, 0, 192, 1) }
.csharpcode .preproc { color: rgba(204, 102, 51, 1) }
.csharpcode .asp { background-color: rgba(255, 255, 0, 1) }
.csharpcode .html { color: rgba(128, 0, 0, 1) }
.csharpcode .attr { color: rgba(255, 0, 0, 1) }
.csharpcode .alt { background-color: rgba(244, 244, 244, 1); width: 100%; margin: 0 }
.csharpcode .lnum { color: rgba(96, 96, 96, 1) }
两者比较
为了方便比较,我们可以将ProductName设置成了private set,然后通过ILSpy来查看一下编译后的代码,看看编译后的Id和ProductName有何不同
咋一看,貌似没啥区别,都使用到了initonly来修饰。但是如果仅仅只是替换声明方式,那么这个新特性似乎就没有什么意义了。
接下来我们看第二张图:
如图标记的那样,区别还是很明显的,通过init修饰的属性并没有完全替换掉set,由此看来微软在设计init的时候,还是挺用心思的,也为后面的赋值留下了入口。
1: instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) set_Id (
2: int32 'value'
3: )
{ font-size: small; color: rgba(0, 0, 0, 1); font-family: consolas, "Courier New", courier, monospace; background-color: rgba(255, 255, 255, 1) }
.csharpcode pre { margin: 0 }
.csharpcode .rem { color: rgba(0, 128, 0, 1) }
.csharpcode .kwrd { color: rgba(0, 0, 255, 1) }
.csharpcode .str { color: rgba(0, 96, 128, 1) }
.csharpcode .op { color: rgba(0, 0, 192, 1) }
.csharpcode .preproc { color: rgba(204, 102, 51, 1) }
.csharpcode .asp { background-color: rgba(255, 255, 0, 1) }
.csharpcode .html { color: rgba(128, 0, 0, 1) }
.csharpcode .attr { color: rgba(255, 0, 0, 1) }
.csharpcode .alt { background-color: rgba(244, 244, 244, 1); width: 100%; margin: 0 }
.csharpcode .lnum { color: rgba(96, 96, 96, 1) }另外在赋值的时候,使用private set修饰的属性,需要定义构造函数,通过构造函数赋值。而使用了init修饰的属性,则不需要定义构造函数,直接在对象初始化器中赋值即可。
1: Product product = new Product
2: {
3: Id = 1,
4: ProductName = "test001",
5: Description = "Just a description"
6: };
7:
8: product.Id = 2;//Error CS8852 Init-only property or indexer 'Product.Id' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.
{ font-size: small; color: rgba(0, 0, 0, 1); font-family: consolas, "Courier New", courier, monospace; background-color: rgba(255, 255, 255, 1) }
.csharpcode pre { margin: 0 }
.csharpcode .rem { color: rgba(0, 128, 0, 1) }
.csharpcode .kwrd { color: rgba(0, 0, 255, 1) }
.csharpcode .str { color: rgba(0, 96, 128, 1) }
.csharpcode .op { color: rgba(0, 0, 192, 1) }
.csharpcode .preproc { color: rgba(204, 102, 51, 1) }
.csharpcode .asp { background-color: rgba(255, 255, 0, 1) }
.csharpcode .html { color: rgba(128, 0, 0, 1) }
.csharpcode .attr { color: rgba(255, 0, 0, 1) }
.csharpcode .alt { background-color: rgba(244, 244, 244, 1); width: 100%; margin: 0 }
.csharpcode .lnum { color: rgba(96, 96, 96, 1) }
如上代码所示,只读属性Id的赋值并没有在构造函数中赋值,毕竟当一个类的只读字段十分多的时候,构造函数也变得复杂。而且在赋值好之后,无法修改,这和我们对只读属性在通常情况下的理解是一致的。另外通过init修饰的好处便是省却了一部分只读属性在操作上的复杂性,使得对象的声明与赋值更加直观。
在合适的场景下选择最好的编程方式,是程序员的一贯追求,千万不要为了炫技而把init当成了茴字的第N种写法到处去问。
record
record是一个非常有用的特性,它是不可变类型,其相等性是通过内部的几个属性来确定的,同时它支持我们以更加方便的方式、像定义值类型那样来定义不可变引用类型。
我们把之前的Product类改成record类型,如下所示:
1: public record Product
2: {
3: public Product(int id, string productName, string description) => (Id, ProductName, Description) = (id, productName, description);
4:
5: public int Id { get; }
6:
7: public string ProductName { get; }
8:
9: public string Description { get; }
10: }
{ font-size: small; color: rgba(0, 0, 0, 1); font-family: consolas, "Courier New", courier, monospace; background-color: rgba(255, 255, 255, 1) }
.csharpcode pre { margin: 0 }
.csharpcode .rem { color: rgba(0, 128, 0, 1) }
.csharpcode .kwrd { color: rgba(0, 0, 255, 1) }
.csharpcode .str { color: rgba(0, 96, 128, 1) }
.csharpcode .op { color: rgba(0, 0, 192, 1) }
.csharpcode .preproc { color: rgba(204, 102, 51, 1) }
.csharpcode .asp { background-color: rgba(255, 255, 0, 1) }
.csharpcode .html { color: rgba(128, 0, 0, 1) }
.csharpcode .attr { color: rgba(255, 0, 0, 1) }
.csharpcode .alt { background-color: rgba(244, 244, 244, 1); width: 100%; margin: 0 }
.csharpcode .lnum { color: rgba(96, 96, 96, 1) }
然后查看一下IL,可以看到record会被编译成类,同时继承了System.Object,并实现了IEquatable泛型接口。
编译器为我们提供的几个重要方法如下:
- Equals
- GetHashCode()
- Clone
- PrintMembers和ToString()
比较重要的三个方法
Equals:
通过图片中的代码,我们知道比较两个record对象,首先需要比较类型是否相同,然后再依次比较内部属性。
GetHashCode():
record类型通过基类型以及所有的属性及字段的方式来计算HashCode,这在整个继承层次结构中增强了基于值的相等性,也就意味着两个同名同姓的人不会被认为是同一个人
Clone:
这个方法貌似非常简单,实在看不出有什么特别的地方,那么我们通过后面的内容再来解释这个方法。
record在DDD值对象中的应用
record之前的定义方式:
了解DDD值对象的小伙伴应该想到了,record类型的特性非常像DDD中关于值对象的描述,比如不可变性、其相等于是基于其内部的属性的等等,我们先来看下值类型的定义方式。
1: public abstract class ValueObject
2: {
3: public static bool operator ==(ValueObject left, ValueObject right)
4: {
5: if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
6: {
7: return false;
8: }
9: return ReferenceEquals(left, null) || left.Equals(right);
10: }
11:
12: public static bool operator !=(ValueObject left, ValueObject right)
13: {
14: return !(left == right);
15: }
16:
17: protected abstract IEnumerable<object> GetEqualityComponents();
18:
19:
20: public override bool Equals(object obj)
21: {
22: if (obj == null || obj.GetType() != GetType())
23: {
24: return false;
25: }
26:
27: var other = (ValueObject)obj;
28:
29: return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
30: }
31:
32: public override int GetHashCode()
33: {
34: return GetEqualityComponents()
35: .Select(x => x != null ? x.GetHashCode() : 0)
36: .Aggregate((x, y) => x ^ y);
37: }
38: // Other utility methods
39: }
40: public class Address : ValueObject
41: {
42: public string Street { get; private set; }
43: public string City { get; private set; }
44: public string State { get; private set; }
45: public string Country { get; private set; }
46: public string ZipCode { get; private set; }
47:
48: public Address(string street, string city, string state, string country, string zipcode)
49: {
50: Street = street;
51: City = city;
52: State = state;
53: Country = country;
54: ZipCode = zipcode;
55: }
56:
57: protected override IEnumerable<object> GetEqualityComponents()
58: {
59: // Using a yield return statement to return each element one at a time
60: yield return Street;
61: yield return City;
62: yield return State;
63: yield return Country;
64: yield return ZipCode;
65: }
66:
67: public override string ToString()
68: {
69: return $"Street: {Street}, City: {City}, State: {State}, Country: {Country}, ZipCode: {ZipCode}";
70: }
71: }
{ font-size: small; color: rgba(0, 0, 0, 1); font-family: consolas, "Courier New", courier, monospace; background-color: rgba(255, 255, 255, 1) }
.csharpcode pre { margin: 0 }
.csharpcode .rem { color: rgba(0, 128, 0, 1) }
.csharpcode .kwrd { color: rgba(0, 0, 255, 1) }
.csharpcode .str { color: rgba(0, 96, 128, 1) }
.csharpcode .op { color: rgba(0, 0, 192, 1) }
.csharpcode .preproc { color: rgba(204, 102, 51, 1) }
.csharpcode .asp { background-color: rgba(255, 255, 0, 1) }
.csharpcode .html { color: rgba(128, 0, 0, 1) }
.csharpcode .attr { color: rgba(255, 0, 0, 1) }
.csharpcode .alt { background-color: rgba(244, 244, 244, 1); width: 100%; margin: 0 }
.csharpcode .lnum { color: rgba(96, 96, 96, 1) }
main方法如下:
1: static void Main(string[] args)
2: {
3: Address address1 = new Address("aaa", "bbb", "ccc", "ddd", "fff");
4: Console.WriteLine($"address1: {address1}");
5:
6: Address address2 = new Address("aaa", "bbb", "ccc", "ddd", "fff");
7: Console.WriteLine($"address2: {address2}");
8:
9: Console.WriteLine($"address1 == address2: {address1 == address2}");
10:
11: string jsonAddress1 = address1.ToJson();
12: Address jsonAddress1Deserialize = jsonAddress1.FromJson<Address>();
13: Console.WriteLine($"jsonAddress1Deserialize == address1: {jsonAddress1Deserialize == address1}");
14:
15: Console.ReadKey();
16: }
{ font-size: small; color: rgba(0, 0, 0, 1); font-family: consolas, "Courier New", courier, monospace; background-color: rgba(255, 255, 255, 1) }
.csharpcode pre { margin: 0 }
.csharpcode .rem { color: rgba(0, 128, 0, 1) }
.csharpcode .kwrd { color: rgba(0, 0, 255, 1) }
.csharpcode .str { color: rgba(0, 96, 128, 1) }
.csharpcode .op { color: rgba(0, 0, 192, 1) }
.csharpcode .preproc { color: rgba(204, 102, 51, 1) }
.csharpcode .asp { background-color: rgba(255, 255, 0, 1) }
.csharpcode .html { color: rgba(128, 0, 0, 1) }
.csharpcode .attr { color: rgba(255, 0, 0, 1) }
.csharpcode .alt { background-color: rgba(244, 244, 244, 1); width: 100%; margin: 0 }
.csharpcode .lnum { color: rgba(96, 96, 96, 1) }
运行结果如下:
1: 基于class:
2: address1: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fff
3: address2: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fff
4: address1 == address2: True
5: jsonAddress1Deserialize == address1: True
.csharpcode, .csharpcode pre { font-size: small; color: rgba(0, 0, 0, 1); font-family: consolas, "Courier New", courier, monospace; background-color: rgba(255, 255, 255, 1) }
.csharpcode pre { margin: 0 }
.csharpcode .rem { color: rgba(0, 128, 0, 1) }
.csharpcode .kwrd { color: rgba(0, 0, 255, 1) }
.csharpcode .str { color: rgba(0, 96, 128, 1) }
.csharpcode .op { color: rgba(0, 0, 192, 1) }
.csharpcode .preproc { color: rgba(204, 102, 51, 1) }
.csharpcode .asp { background-color: rgba(255, 255, 0, 1) }
.csharpcode .html { color: rgba(128, 0, 0, 1) }
.csharpcode .attr { color: rgba(255, 0, 0, 1) }
.csharpcode .alt { background-color: rgba(244, 244, 244, 1); width: 100%; margin: 0 }
.csharpcode .lnum { color: rgba(96, 96, 96, 1) }
采用record方式定义:
如果有大量的值对象需要我们编写,这无疑是加重我们的开发量的,这个时候record就派上用场了,最简洁的record风格的代码如下所示,只有一行:
1: public record Address(string Street, string City, string State, string Country, string ZipCode);
{ font-size: small; color: rgba(0, 0, 0, 1); font-family: consolas, "Courier New", courier, monospace; background-color: rgba(255, 255, 255, 1) }
.csharpcode pre { margin: 0 }
.csharpcode .rem { color: rgba(0, 128, 0, 1) }
.csharpcode .kwrd { color: rgba(0, 0, 255, 1) }
.csharpcode .str { color: rgba(0, 96, 128, 1) }
.csharpcode .op { color: rgba(0, 0, 192, 1) }
.csharpcode .preproc { color: rgba(204, 102, 51, 1) }
.csharpcode .asp { background-color: rgba(255, 255, 0, 1) }
.csharpcode .html { color: rgba(128, 0, 0, 1) }
.csharpcode .attr { color: rgba(255, 0, 0, 1) }
.csharpcode .alt { background-color: rgba(244, 244, 244, 1); width: 100%; margin: 0 }
.csharpcode .lnum { color: rgba(96, 96, 96, 1) }
IL代码如下图所示,从图中我们也可以看到record类型的对象,默认情况下用到了init来限制属性的只读特性。
main方法代码不变,运行结果也没有因为Address从class变成record而发生改变
1: 基于record:
2: address1: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fff
3: address2: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fff
4: address1 == address2: True
5: jsonAddress1Deserialize == address1: True
{ font-size: small; color: rgba(0, 0, 0, 1); font-family: consolas, "Courier New", courier, monospace; background-color: rgba(255, 255, 255, 1) }
.csharpcode pre { margin: 0 }
.csharpcode .rem { color: rgba(0, 128, 0, 1) }
.csharpcode .kwrd { color: rgba(0, 0, 255, 1) }
.csharpcode .str { color: rgba(0, 96, 128, 1) }
.csharpcode .op { color: rgba(0, 0, 192, 1) }
.csharpcode .preproc { color: rgba(204, 102, 51, 1) }
.csharpcode .asp { background-color: rgba(255, 255, 0, 1) }
.csharpcode .html { color: rgba(128, 0, 0, 1) }
.csharpcode .attr { color: rgba(255, 0, 0, 1) }
.csharpcode .alt { background-color: rgba(244, 244, 244, 1); width: 100%; margin: 0 }
.csharpcode .lnum { color: rgba(96, 96, 96, 1) }
如此看来我们的代码节省的不止一点点,而是太多太多了,是不是很爽啊。
record对象属性值的更改
使用方式如下:
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: Address address1 = new Address("aaa", "bbb", "ccc", "ddd", "fff");
6: Console.WriteLine($"1. address1: {address1}");
7:
8: Address addressWith = address1 with { Street = "############" };
9:
10: Console.ReadKey();
11: }
12: }
13:
14: public record Address(string Street, string City, string State, string Country, string ZipCode);
{ font-size: small; color: rgba(0, 0, 0, 1); font-family: consolas, "Courier New", courier, monospace; background-color: rgba(255, 255, 255, 1) }
.csharpcode pre { margin: 0 }
.csharpcode .rem { color: rgba(0, 128, 0, 1) }
.csharpcode .kwrd { color: rgba(0, 0, 255, 1) }
.csharpcode .str { color: rgba(0, 96, 128, 1) }
.csharpcode .op { color: rgba(0, 0, 192, 1) }
.csharpcode .preproc { color: rgba(204, 102, 51, 1) }
.csharpcode .asp { background-color: rgba(255, 255, 0, 1) }
.csharpcode .html { color: rgba(128, 0, 0, 1) }
.csharpcode .attr { color: rgba(255, 0, 0, 1) }
.csharpcode .alt { background-color: rgba(244, 244, 244, 1); width: 100%; margin: 0 }
.csharpcode .lnum { color: rgba(96, 96, 96, 1) }
通过ILSpy查看如下所示:
1: private static void Main(string[] args)
2: {
3: Address address1 = new Address("aaa", "bbb", "ccc", "ddd", "fff");
4: Console.WriteLine($"1. address1: {address1}");
5: Address address2 = address1.<Clone>$();
6: address2.Street = "############";
7: Address addressWith = address2;
8: Console.ReadKey();
9: }
{ font-size: small; color: rgba(0, 0, 0, 1); font-family: consolas, "Courier New", courier, monospace; background-color: rgba(255, 255, 255, 1) }
.csharpcode pre { margin: 0 }
.csharpcode .rem { color: rgba(0, 128, 0, 1) }
.csharpcode .kwrd { color: rgba(0, 0, 255, 1) }
.csharpcode .str { color: rgba(0, 96, 128, 1) }
.csharpcode .op { color: rgba(0, 0, 192, 1) }
.csharpcode .preproc { color: rgba(204, 102, 51, 1) }
.csharpcode .asp { background-color: rgba(255, 255, 0, 1) }
.csharpcode .html { color: rgba(128, 0, 0, 1) }
.csharpcode .attr { color: rgba(255, 0, 0, 1) }
.csharpcode .alt { background-color: rgba(244, 244, 244, 1); width: 100%; margin: 0 }
.csharpcode .lnum { color: rgba(96, 96, 96, 1) }
由此可以看到record在更改的时候,实际上是通过调用Clone而产生了浅拷贝的对象,这也非常符合DDD ValueObject的设计理念。
参考:
- https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/implement-value-objects
- https://deviq.com/value-object/
C# 9.0中引入的新特性init和record的使用思考的更多相关文章
- Jdk5.0中出现的新特性
掌握jdk5.0中出现的新特性1.泛型(Generics)2.增强的"for"循环(Enhanced For loop)3.自动装箱/自动拆箱(Autoboxing/unboxin ...
- C#6.0 中的那些新特性
C#6.0 中的那些新特性 前言 VS2015在自己机器上确实是装好了,费了老劲了,想来体验一下跨平台的快感,结果被微软狠狠的来了一棒子了,装好了还是没什么用,应该还需要装Xarmain插件,配置一些 ...
- VS2015 C#6.0 中的那些新特性(转载)
自动属性初始化 (Initializers for auto-properties) 以前我们是这么写的 为一个默认值加一个后台字段是不是很不爽,现在我们可以这样写 只读属性的初始化(Getter-o ...
- VS2015 C#6.0 中的那些新特性(转自http://www.cnblogs.com/henryzhu/p/new-feature-in-csharp-6.html)
自动属性初始化 (Initializers for auto-properties) 以前我们是这么写的 为一个默认值加一个后台字段是不是很不爽,现在我们可以这样写 只读属性的初始化(Getter-o ...
- C#6.0中10大新特性的应用和总结
微软发布C#6.0.VS2015等系列产品也有一段时间了,但是网上的教程却不多,这里真对C#6.0给大家做了一些示例,分享给大家. 微软于2015年7月21日发布了Visual Studio 20 ...
- Android3.0中ActionBar的新特性
1. ActionBar(活动栏)替代了显示在屏幕顶端的标题栏.主要负责显示菜单,widget,导航等功能,主要包括:@ 显示选项菜单中的菜单项到活动栏:@ 添加可交互的视图到活动栏作为活动视图: ...
- VS2015 C#6.0 中的那些新特性
本人个人博客原文链接地址为http://aehyok.com/Blog/Detail/66.html. 个人网站地址:aehyok.com QQ 技术群号:206058845,验证码为:aehyok ...
- Android 7.0(牛轧糖)新特性
Android 7.0(牛轧糖)新特性 谷歌正式在I/O大会现场详细介绍了有关Android 7.0的大量信息.目前,我们已经知道,新一代Android操作系统将支持无缝升级,能够通过Vulkan A ...
- Xcode中StoryBoard Reference 新特性的使用
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,bi ...
随机推荐
- DockerInstall
1.安装Tomcat 2.安装mysql [1].pull [root@pluto tomcat7logs]# docker pull mysql:5.6 [root@pluto tomcat7log ...
- linux centos 6.x 装机后基本优化
1.关闭SELinux /etc/selinux/config配置文件内替换 se -i 's/SELINUX=enforcing/SELINUX=disabled/g'需要重启grep SELINU ...
- JVM(一)-JVM入门
JVM的定义: JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的 ...
- python编码规范以及推导式的编写
一.python 的编码规范
- HotSpot类模型之ArrayKlass
上一篇分析了 HotSpot类模型之InstanceKlass ,这次主要分析表示java数组类型的C++类. 1.ArrayKlass类 ArrayKlass继承自Klass,是所有数组类的抽象基类 ...
- pikachs 渗透测试2-XSS漏洞及利用
一.概述 XSS(跨站脚本)概述 Cross-Site Scripting 简称为"CSS",为避免与前端叠成样式表的缩写"CSS"冲突,故又称XSS.一般XS ...
- 重闯Sqli-labs关卡第三天(11-15关)
前言 今天早上学了学网易云课堂的注入课程突然感觉网易云可i春秋联合出品的课程还不错,之后就刷了刷攻防世界的题,题有眉目但是还是忍不住看了WP 服了自己了!!!一直刷到了晚上也不知道几点我妈买了柚子回了 ...
- sql字段长度等于
select count(*) from boc_loan_apply where length(birthday)=7;
- MindManager使用教程:如何导出HTML5交互式导图
Mindmanager思维导图软件有着友好的用户界面以及丰富的思维导图制作功能.再搭配与Microsoft 软件的无缝集成功能,使得这款思维导图软件越来越受到职场人士的喜爱. 不仅是作为制作思维导图的 ...
- 怎么用MindManager自带的模板和设计画思维导图
小编知道大家平时工作学习都很忙,思维导图能完成的效率越高越好.所以今天,小编就为大家介绍两个能高效使用思维导图软件完成制作思维导图的小技巧.保证内容充实美观,还不费时间. 一.使用模板 打开MindM ...