前言:写此文章一方面是为了巩固对序列化的认识,另一方面是因为本人最近在面试,面试中被问到“为什么要序列化”。虽然一直在使用,自己也反复的提到序列化,可至于说为什么要序列化,还真的没想过,所以本文就这样产生了。

序列化是将一个对象转换成一个字节流的过程。反序列化是将一个字节流转换回对象的过程。在对象和字节流之间转换是很有用的一个机制。(当然这个还不能回答它的实际用处)

举点例子:

  • 应用程序的状态可以保存到一个磁盘文件或数据库中,并在应用程序下次运行时恢复。比如ASP.NET就是利用系列化和反序列化保存和恢复回话状态。
  • 一组对象可以轻松复制到系统的剪切板,然后再粘贴到其他的地方(应用程序)。
  • 一组对象可克隆并放到其他地方作为备份。
  • 一组对象可以通过网络发送给另一台机器上运行的进程(比如Remoting)。

除了上述的几个场景,我们可以将系列化得到的字节流进行任意的操作。

一、序列化、反序列化快速实践

  1. [Serializable]
  2. class MyClass
  3. {
  4. public string Name { get; set; }
  5. }

一个自定义类,切记需要加上[Serializable]特性(可应用于class、struct、enum、delegate)。

  1. private static MemoryStream SerializeToMemoryStream(object objectGraph)
  2. {
  3. //一个流用来存放序列化对象
  4. var stream = new MemoryStream();
  5. //一个序列化格式化器
  6. var formater = new BinaryFormatter();
  7. //将对象序列化到Stream中
  8. formater.Serialize(stream, objectGraph);
  9. return stream;
  10. }
  11.  
  12. private static object DeserializeFromMemory(Stream stream)
  13. {
  14. var formater = new BinaryFormatter();
  15. return formater.Deserialize(stream);
  16. }

SerializeToMemoryStream为序列化方法,此处通过BinaryFormatter类将对象序列化到MemoryStream中,然后返回Stream对象。

DeserizlizeFromMemory为反序列化方法,通过传入的Stream,然后使用BinaryFormatter的Deserialize方法反序列化对象。

除了可以使用BinaryFormatter进行字节流的序列化,还可以使用XmlSerializer(将对象序列为XML)和DataContratSerializer。

Serialize的第二个参数是一个对象的引用,理论上应该可以是任何类型,不管.net的基本类型还是其他类型或者是我们的自定义类型。如果是对象和对象的引用关系,Serizlize也是可以一直序列化的,而且Serialize会很智能的序列化每个对象都只序列化一次,防止进入无限循环。

P.S. 1.Serialze方法其实可以将对象序列化为Stream,也就意味着不仅可以序列化为MemoryStream,还可以序列化为FIleStream或者是其他继承自Stream的类型。

2.除了上述的将一个对象序列化到一个Stream,也可以将多个对象序列化中,还是调用Serialize方法,第二个参数为不同的对象即可;在反序列化的时候同样的方法,只不过      强转的类型指定为需要的即可。

序列化多个对象到Stream:

  1. MyClass class1 = new MyClass();
  2. MyClass2 class2=new MyClass2();
  3. formater.Serialize(stream,class1);
  4. formater.Serialize(stream,class2);

从Stream中反序列化多个对象:

  1. MyClass class1 =(MyClass) formater.Deserialize(stream);
  2. MyClass1 class2 = (MyClass1)formater.Deserialize(stream);

二、控制序列化和反序列化

如果给类添加了SerializeAttribute,那么类的所有实例字段(private、protected、public等)都会被序列化。但是,有时候类型中定义了一些不应序列化的实例字段。

一般情况下,以下两种情况不希望序列化字段:

  • 字段含有反序列化后变得无效的信息。例如,假定一个对象包含到一个Windows内核对象(如文件、进程、线程、事件等),那么在反序列化到另一个进程或另一台机器之后,就会失去意义。
  • 字段含有很容易计算的信息。在这种情况下,要选出那些无需序列化的字段,减少需要传输的数据,从而增强应用程序的性能。

使用NonSerializedAttribute特性来指明哪些字段无需序列化。

  1.     [NonSerialized]
  2. private string _name;

p.s.[NoSerialized] 仅仅能添加在字段,或者是没有get和set访问器属性上,对于有get和set这样的属性使用是不行的。没关系使用[ScriptIgnore]特性标识属性则可以忽略JSON这样的序列化、使用[XmlIgnoreAttribute]特性标识属性则可以忽略XmlSerializer的序列化操作。

虽然使用NonSerizlized特性可以使字段不被序列化,但是在序列化或者反序列化的时候往往都会把值清空,或者是没有一些希望的默认值,还好我们可以使用其他的特性来辅助完成。

修改下上文中的MyClass:

  1. [Serializable]
  2. class MyClass
  3. {
  4. [NonSerialized]
  5. public string _name;
  6.  
  7. [OnDeserialized]
  8. private void OnDeserialized(StreamingContext context)
  9. {
  10. _name = "Mario";
  11. }
  12.  
  13. [OnDeserializing]
  14. private void OnDeserializing(StreamingContext context)
  15. {
  16. _name = "super";
  17. }
  18.  
  19. [OnSerializing]
  20. private void OnSerializing(StreamingContext context)
  21. {
  22. _name = "listen";
  23. }
  24.  
  25. [OnSerialized]
  26. private void OnSerialized(StreamingContext context)
  27. {
  28. _name = "fly";
  29. }
  30.  
  31. public void Print()
  32. {
  33. Console.WriteLine(_name);
  34. }
  35. }

在类中一共使用了四个特性,OnDeserialized、OnDeserializing、OnSerializing、OnSerialized,分别是反序列化后、反序列化前、序列化前、序列化后。不过,如果同时指定了OnDeserialized和OnDeserializing,那么结果应该是OnDeserialized中的逻辑;同理,如果同时指定了OnSerializing和OnSerialized,那么结果应该是OnSerialized中的逻辑。另外,在一个类中,仅仅能指定一个方法为上述中的一个特性(即OnSerialized特性只能被一个方法使用、OnSerialized特性只能被一个方法使用,其余两个同理),否则序列化或者反序列化则会出现异常。

P.S. 这些方法通常为private的,并且参数为StreamingContext。

  1.       MyClass class1 = new MyClass();
  2. var stream = SerializeToMemoryStream(class1);
  3. class1.Print();
  4. stream.Position = ;
  5. class1 = (MyClass)DesrializeFromMemory(stream);
  6. class1.Print();
  7. Console.Read();

运行上述调用可以发现,虽然我们没有将name属性序列化,但是在序列化/反序列化之后还是可以输出值的,如果你同时指定了OnDeserializing和OnDeserialized或者同时指定了OnSerializing和OnSerialized,那么你会发现使用的都是后者的值,这也验证了上述中的解释。

有时候我们的类可能会增加字段,可是呢,我们已经序列化好的数据是旧的版本,所以在反序列化的时候就会出现异常,还好我们也有办法,给新加的字段都增加一个OptinalFieldAttribute特性,这样当格式化器看到该attribute应用于一个字段时,就不会因为流中的数据不包含这个字段而出现异常。

三、序列化和反序列化的原理

为了简化格式化器的操作,在System.Runteime.Serialization中有一个FormatterServices类型。该类型只包含静态方法,并且该类为静态类。

Serialize步骤:

  • 格式化器调用FormatterServices的GetSerializableMembers方法:

    1. public static MemberInfo[] GetSerializableMembers(Type type,StreamContext context);

    这个方法利用反射获取类型的public和private实例字段(除了标识为NonSerializedAttribute的字段除外)。方法返回由MemberInfo对象构成的一个数组,其中每个元素都对应于一个可序列化的实例字段。

  • 对象被序列化,MemberInfo对象数组传给FormatterServices的静态方法GetObjectData:
    1. public static object[] GetObjectData(Object obj,MemberInfo[] members);

    这个方法返回一个Object数组,其中每个元素都标识了被序列化的那个对象的一个字段的值。这个Object数组和MemberInfo数组是并行的;也就是说,Object数组中的元素0是MemberInfo数组中的元素0所标识的那个成员的值。

  • 格式化器将程序集标识和类型的完整名称写入流中。
  • 格式化器然后遍历两个数组中的元素,将每个成员的名称和值写入流中。

Deserialize步骤:

  • 格式化器从流中读取程序集标识和完整类型名称。如果程序集当前没有加载到AppDomain中,就加载它。如果程序集不能加载,则出现异常。如果程序集已经加载,格式化器将程序集标识信息和类型全名传给FormatterServices的静态方法GetTypeFromAssembly:

    1. public static Type GetTypeFromAssembly(Assembly assembly, string name);

    这个方法返回一个Type对象,代表要反序列化的那个对象的类型。

  • 格式化器调用FormatterServices的静态方法GetUninitializedObject:
    1. public static Object GetUninitializedObject(Type type);

    这个方法为一个新对象分配内存,并不为对象调用构造函数。所以,对象的所有字段都被初始化为null或者0;

  • 格式化器现在构造并初始化一个MemberInfo数组,同样是调用FormatterServices的GetSerializableMembers方法。这个方法返回序列化好,需要反序列化的一组字段。
  • 格式化器根据流中包含的数据创建并初始化一个Object数组。
  • 将对新分配的对象、MemberInfo数组以及并行Object数组的传给FomatterServices的静态方法PopulateObjectMembers:
    1. public static Object PopulateObjectMembers(Object obj,MemberInfo[] members, Object [] data);

    这个方法遍历数组,将每个字段初始化成对应的值。到这里,就算反序列化结束了。

四、控制序列化/反序列化的数据

本文上述,有提到如何使用OnSerializing、OnSerialized、OnDeserializing、OnDeserialized以及NonSerialized和OptionalField特性进行控制序列化和反序列化。但是,格式化器内部使用反射,而反射的速度是比较慢的,所以增加了序列化和反序列化对象所花的时间。为了对序列化和反序列化完全的控制,并且不使用反射,那么我们的类型可以实现ISerializable接口,此接口仅仅有一个方法:

  1. public Interface ISerializable
  2. {
  3. void GetObjectData(SerializationInfo info, StreamContext context);
  4. }

一旦类型实现了此接口,所有派生类型也必须实现它,而且派生类型必须保证调用基类的GetOBjectData方法和特殊的构造器。除此之外,一旦类型实现了该接口,则永远不能删除它,否则会失去与派生类的兼容性。

ISerializable接口和特殊构造器旨在由格式化器使用。但是,任何代码都可能调用GetObjectData,则可能返回敏感数据。另外,其他代码可能构造一个对象,并传入损坏的数据。因此,建议将如下的attribute应用于GetObjectData方法和特殊构造器:

  1. [SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter = true)]

格式化器序列化一个对象时,会检查每个对象。如果发现一个对象的类型实现了ISerializable接口,格式化器就会忽略所有定制attribute,改为构造一个新的SerializationInfo对象,这个对象包含了要实际为对象序列化的值的集合。

构造一个SerializationInfo时,格式化器要两个参数:Type和IFormatterConverter。Type参数标识要序列化的对象。为了唯一性地标识一个类型,需要两个部分的信息:类型的字符串名称及其程序集的标识。一个SerializationInfo对象构造好之后,会包含类型的全名(即Type的FullName),并将这个字符串存储到一个私有字段中。为了获取类型的全名,可使用SerializationInfo的FullTypeName属性。通过调用SerializationInfo的SetType方法,传递目标Type对象的引用,用于设置FullTypeName和AssemblyName属性。

构造好并初始化SerializationInfo对象后,格式化器调用类型的GetObjectData方法,传递SeriializationInfo对象。GetObjectData方法负责决定需要序列化的信息,然后将这些信息添加到SerializationInfo中。GetObjectData调用SerializationInfo类型的AddValue方法来指定要序列化的信息。需要对每个要添加的数据,都进行AddValue方法的调用。

下面代码展示了Dictionary<TKey,TValue>类型如何实现ISerializable和IDeserializationCallback接口来控制其对象的序列化和反序列化工作。

四、在基类没有实现ISerializable的情况下定义一个实现它的类型

之前提到,如果基类实现了ISerializable接口,那么它的派生类也必须实现ISerializable接口,同时还要调用基类的GetObjectData方法和特殊构造器。(见上文红色字体)
但是,你可能要定义一个类型来控制它的序列化,但它的基类没有实现ISerializable接口。在这种情况下,派生类必须手动序列化基类的字段,具体的做法是获取它们的值,并把这些值添加到SerializationInfo集合中。然后,在特殊构造器中,还必须从集合中取出值,并以某种方式设置基类的字段。如果基类的字段是public或者protected字段,还容易实现。但,如果基类的private字段,那么则很难实现。

以下代码实现如何正确实现ISerializable的GetObjectData方法和特殊的构造器:

  1. [Serializable]
  2. class Base
  3. {
  4. protected string name = "Mario";
  5. public Base()
  6. {
  7. }
  8. }
  9.  
  10. [Serializable]
  11. class Derived : Base, ISerializable
  12. {
  13. private DateTime _date = DateTime.Now;
  14. public Derived() { }

  15.       //如果这个构造器不存在,则会引发一个SerializationException异常
          //如果此类不是密封类,这个构造器就应该是protected的
  16. [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
  17. private Derived(SerializationInfo info, StreamingContext context)
  18. {
  19. Type baseType = this.GetType().BaseType;
  20. MemberInfo[] memberInfos = FormatterServices.GetSerializableMembers(baseType, context);
  21.  
  22. for (int i = ; i < memberInfos.Length; i++)
  23. {
  24. FieldInfo fieldInfo = (FieldInfo)memberInfos[i];
  25. fieldInfo.SetValue(this, info.GetValue(baseType.FullName + "+" + fieldInfo.Name, fieldInfo.FieldType));
  26. }
  27. _date = info.GetDateTime("Date");
  28. }
  29.  
  30. [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
  31. public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
  32. {
  33. info.AddValue("Data", _date);
  34.  
  35. Type baseType = this.GetType().BaseType;
  36. MemberInfo[] memberInfos = FormatterServices.GetSerializableMembers(baseType,context);
  37.  
  38. for (int i = ; i < memberInfos.Length; i++)
  39. {
  40. info.AddValue(baseType.FullName + "+" + memberInfos[i].Name, ((FieldInfo)memberInfos[i]).GetValue(this));
  41. }
  42. }
  43.  
  44. public override string ToString()
  45. {
  46. return string.Format("Name={0},Date={}", name, _date);
  47. }
  48. }

在代码中,有一个名为Base的基类,它只用Serializable特性标识。其派生类Derived类,也使用了Serializable特性,同时还实现了ISerializable接口。同时两个类还定义了自己的字段,调用SerializationInfo的AddValue方法进行序列化和反序列化。

解释:

序列化: 每个AddValue方法都获取一个String名称和一些数据。数据一般是简单的类型,当然我们也可以传递object引用。GetObjectData添加好所有必要的序列化信息之后,会返回至格式化器。现在,格式化器获取已经添加到SerializationInfo对象的所有值,并把它们都序列化到流中。同时,我们还向GetObjectData方法中传递了另外一个参数StreamingContext对象的实例。当然,大多数类型的GetObjectData方法都忽略了此参数,下文详细说明。

反序列化:格式化器从流中提取一个对象时,会为新对象分配内存(通过FormatterService.GetUninitializedObject方法)。最初,此对象的所有字段都为0或者是null。然后,格式化器检查类型是否实现了ISerializable接口。如果存在此接口,格式化器则会尝试调用我们定义的特殊构造函数,它的参数和GetObjectData是一致的。

如果类是密封类,则建议将此特殊构造声明为private,这样就可以防止其他代码调用它。如果不是密封类,则应该将这个特殊构造器声明为protected,保证派生类可以调用它。切记,无论这个特殊构造器是如何声明的,格式化器都可以调用它的。

构造器获取对一个SerializationInfo对象的引用,在这个SerializationInfo对象中,包含了对象(要序列化的对象)序列化时添加的所有值。特殊构造器可调用GetBoolean,GetChar,GetByte,GetInt32和GetValue等任何一个方法,向他传递与序列化一个值所用的名称对应的一个字符串。以上的每个方法返回的值再用于初始化新对象的各个字段。

反序列化一个对象的字段时,应调用和对象序列化时传给AddValue方法的值得类型匹配的一个Get方法。也就是说,如果GetObjectData方法调用AddValue时传递的是一个Int32值,那么在反序列化对象的时候,也应该为同一个值调用GetInt32方法。如果值在流中的类型和你要获取的类型不匹配,格式化器则会尝试用IFormatterConverter对象将流中的值转换为你指定的类型。

上文中提到,构造SerializationInfo对象时,需要传递Type和IFormatterConverter接口的对象(此时,它是重点,不要被Type勾引走)。由于格式化器负责构造SerializationInfo对象,所以要由它选择它需要的IFormatterConverter。.Net的BinaryFormatter和SoapFormatter构造的就是一个FormatterConverter类型,.Net的格式化器没有提供一个让你可以选择的IFormatterConverter的实现。

FormatterConverter类型调用System.Convert类的各种静态方法在不同的类型之间进行转换,比如讲一个Int16转换为Int32。然而,为了在其他任意类型之间转换一个值,FormatterConverter需要调用Convert的ChangeType方法将序列化好的类型转换为一个IConvertible接口,然后再调用恰当的接口的方法。所以,要允许一个可序列化类型的对象反序列化成一个不同的类型,可以考虑让自己的类型实现IConvertible接口。切记,只有在反序列化对象时调用Get方法,并且发现了类型和流中的值得类型不匹配时候,才会使用FormatterConverter对象。

特殊构造器也可以不调用上面的各种Get方法,而是调用GetEnumerator。此方法会返回一个SerializationInfoEnumerator对象,可使用该对象遍历SerializationInfo对象中包含的所有的值。枚举的每个值都是一个SerializationEntry对象。

当然,我们完全可以自定义一个类型,让它实现ISerializable的GetObjectData方法和特殊构造器一个类型派生。如果我们的类型实现了ISerializable,那么可以在我们实现的GetObjectData方法和特殊构造器中,必须调用基类中的同名方法,以确保对象正确序列化和反序列化。这一点是必须的哦,否则对象时不能正确序列化和反序列化。

如果我们的派生类型中没有其他的额外字段,当然也没有特殊的序列化和反序列化需求,就不用事先ISerializable接口。和其他接口成员相似,GetObjectData是virtual的,调用它可以正确的序列化对象。格式化器将特殊构造器视为“已虚拟化”,也就是说,反序列化过程中,格式化器会检查要实例的类型,如果那个类型没有提供特殊的特殊构造器,则会看其基类是否存在,知道找到一个实现了特殊构造器的一个类。

注意:特殊构造器中的代码一般会从传给 它的SerializationInfo对象中提取字段。提取了字段后,不能保证对象已完全反序列化,所以特殊构造器中的代码不应尝试操纵它提取的对象。如果我们的类型必须访问提取的一个对象中的成员,最好我们的类型提供一个应用了OnDeserialized特性的方法,或者让我们的类型实现IDeserializationCallback接口的OnDeserialization方法。调用该方法时,所有对象的字段都已经设置好。然而,对于多个对象来说,它们的OnDeserialized或OnDeserialization方法的调用顺序是没有保障的。所以,虽然字段可能已经初始化,但我们仍然不知道被引用的对象是否已完全反序列化好(如果那个被引用的对象也提供了一个OnDeserialized方法或者实现了IDeserializationCallback)。

P.S. 必须调用AddValue方法的某个重载版本为自己的类型添加序列化信息。如果一个字段的类型实现了ISerializable接口,就不要在字段上调用GetObjectData,而应该调用AddValue来添加字段。格式化器会发现字段的类型实现了ISerializable,会自动调用GetObjectData。如果自己在字段上调用了GetObjectData,格式化器则不会知道在对流进行反序列化时创建一个新对象。

五、将类型序列化为不同的类型以及将对象反序列化为不同的对象

  1.    [Serializable]
  2. public class Student : ISerializable
  3. {
  4. private string _name;
  5.  
  6. public string Name
  7. {
  8. get { return _name; }
  9. set { _name = value; }
  10. }
  11. [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
  12. public void GetObjectData(SerializationInfo info, StreamingContext context)
  13. {
  14. info.SetType(typeof(SerializationHelper));
  15. }
  16. }
  17.  
  18. [Serializable]
  19. public class SerializationHelper : IObjectReference
  20. {
  21. public object GetRealObject(StreamingContext context)
  22. {
  23. return "新的类型哦";
  24. }
  25. }

上述代码中一个我们的数据类Student,还有一个序列化帮助类,其中Student类就是我们要序列化的类,帮助类就是为了告诉代码我们要把Student类序列化为它,并且再反序列化的时候也应该是它。
测试下:

  1. static void Main(string[] args)
  2. {
  3. Student student = new Student { Name = "马里奥" };
  4. using (var stream = new MemoryStream())
  5. {
  6. BinaryFormatter formatter = new BinaryFormatter();
  7. formatter.Serialize(stream, student);
  8. stream.Position = ;
  9.  
  10. var deserializeValue = formatter.Deserialize(stream);
  11. Console.Write(deserializeValue.ToString());
  12. Console.Read();
  13. }
  14. }

可以看到结果:

P.S. ISerializable:允许对象控制其自己的序列化和反序列化过程。

   IObjectReference:指示当前接口实施者是对另一个对象的引用。

好了,序列化和反序列化的东西说的也差不多了,大家有什么更好的想法可以和我交流。

C#之你懂得的序列化/反序列化的更多相关文章

  1. php json与xml序列化/反序列化

    在web开发中对象的序列化与反序列化经常使用,比较主流的有json格式与xml格式的序列化与反序列化,今天想写个jsop的小demo,结果发现不会使用php序列化,查了一下资料,做个笔记 简单数组js ...

  2. 序列化反序列化api(入门级)

    定义: java序列化是指把Java对象转换为字节序列的过程:而Java反序列化是指把字节序列恢复为Java对象的过程. 为什么字符串通常也会进行序列化? 对象需要进行序列化的原因:保证对象的状态不变 ...

  3. python_way ,day5 模块,模块3 ,双层装饰器,字符串格式化,生成器,递归,模块倒入,第三方模块倒入,序列化反序列化,日志处理

    python_way.day5 1.模块3 time,datetime, json,pickle 2.双层装饰器 3.字符串格式化 4.生成器 5.递归 6.模块倒入 7.第三方模块倒入 8.序列化反 ...

  4. springboot学习(三)——http序列化/反序列化之HttpMessageConverter

    以下内容,如有问题,烦请指出,谢谢! 上一篇说掉了点内容,这里补上,那就是springmvc的http的序列化/反序列化,这里简单说下如何在springboot中使用这个功能. 使用过原生netty ...

  5. java序列化反序列化深入探究

    When---什么时候需要序列化和反序列化: 简单的写一个hello world程序,用不到序列化和反序列化.写一个排序算法也用不到序列化和反序列化.但是当你想要将一个对象进行持久化写入文件,或者你想 ...

  6. java序列化反序列化深入探究(转)

    When---什么时候需要序列化和反序列化: 简单的写一个hello world程序,用不到序列化和反序列化.写一个排序算法也用不到序列化和反序列化.但是当你想要将一个对象进行持久化写入文件,或者你想 ...

  7. C# XML序列化/反序列化参考

    .NET提供了很不错的XML序列化/反序列化器,(它们所在的命名空间为System.Xml.Serialization)这是很方便的,下面对它的使用做一些总结,以供参考. 1,简单序列化 public ...

  8. 二进制数据的序列化反序列化和Json的序列化反序列化的重要区别

    前言:最近一个一个很奇怪的问题,很明白的说,就是没看懂,参照下面的代码: /// <summary> /// 反序列化对象 /// </summary> /// <typ ...

  9. 序列化 反序列化 MessagePack for C#

    阅读目录 快速序列化组件MessagePack介绍 简介 使用 快速开始 分析器 内置的支持类型 对象序列化 DataContract兼容性 序列化不可变对象(序列化构造器) 序列化回调 Union ...

随机推荐

  1. c判断括弧是否匹配

    这里我没有用堆栈.直接用一个数组input[SIZE]接收用户的输入,在遍历数组,对数组进行操作.已经匹配好的括弧直接用#号覆盖,最后遍历数组.如果数组只有#号,没有其他元素,则匹配.否则不匹配. / ...

  2. svg绘制蓝色星空,月亮,旋转灯塔

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http ...

  3. Careercup - Google面试题 - 4699414551592960

    2014-05-06 13:34 题目链接 原题: we have a random list of people. each person knows his own height and the ...

  4. PyDev for Eclipse 简介

    PyDev 安装和配置 安装 PyDev 在安装 PyDev 之前,要保证您已经安装了 Java 1.4 或更高版本.Eclipse 以及 Python.接下来,开始安装 PyDev 插件. 启动 E ...

  5. 链路层三种类型的MAC地址

    若需要转载,请注明出处. 我们知道,链路层都是以MAC地址来进行通信双方的地址标识的,如下图:在应用中根据接收方的多寡来进行划分,可分为以下三种: 单播(Unicast) 多播(Multicast) ...

  6. Entity Framework公共的增删改方法

    using System; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.I ...

  7. Ionic 安装部署

    Ionic 安装部署 准备工作 下载安装Node.js, JDK,Apache Ant,Android SDK:编辑器用WebStorm node jdk ant 均需要加进 环境变量path中 An ...

  8. Sprite Kit 入门教程

    Sprite Kit 入门教程  Ray Wenderlich on September 30, 2013 Tweet 这篇文章还可以在这里找到 英语, 日语 If you're new here, ...

  9. 2-Highcharts 3D图之3D柱状图带可调试倾斜角度

    <!DOCTYPE> <html lang='en'> <head> <title>2-Highcharts 3D图之3D柱状图带可调试倾斜角度< ...

  10. 引擎设计跟踪(九.8) Gizmo helper实现与多国语言

    最近把gizmo helper的绘制做好了. 1.为了复用代码,写了utility来创建sphere, cube, cylinder, plane, ring(line), circle(solid) ...