46、C 中的属性与序列化:深入解析与实践

C# 中的属性与序列化:深入解析与实践

在 C# 编程中,属性(Attributes)是一种强大的元数据机制,它可以为代码元素(如类、方法、属性等)添加额外的信息。本文将深入探讨 C# 中属性的使用,包括自定义属性的限制、预定义属性的特性以及序列化相关属性的应用。

1. System.AttributeUsageAttribute

大多数属性仅用于装饰特定的构造。为避免属性的不当使用,可以使用 System.AttributeUsageAttribute 来限制属性的使用范围。

例如,以下代码展示了如何限制 CommandLineSwitchAliasAttribute 只能用于属性:

[AttributeUsage(AttributeTargets.Property)] 
public class CommandLineSwitchAliasAttribute : Attribute 
{
  // ... 
}

如果在不允许的地方使用该属性,如以下代码:

// ERROR: The attribute usage is restricted to properties 
[CommandLineSwitchAlias("?")] 
class CommandLineInfo 
{ 
}

将会导致编译时错误:

...Program+CommandLineInfo.cs(24,17): error CS0592: Attribute 
’CommandLineSwitchAlias’ is not valid on this declaration type. It is 
valid on ’property, indexer’ declarations only.

AttributeUsageAttribute 的构造函数接受一个 AttributesTargets 标志,该枚举列出了运行时允许属性装饰的所有可能目标。例如,如果还允许 CommandLineSwitchAliasAttribute 用于字段,可以更新 AttributeUsageAttribute 类:

// Restrict the attribute to properties and methods
public class CommandLineSwitchAliasAttribute : Attribute 
{
  // ... 
}
2. 命名参数

除了限制属性的装饰范围, AttributeUsageAttribute 还提供了一种机制,允许在单个构造上使用相同属性的重复实例。以下是使用命名参数的示例:

[AttributeUsage(AttributeTargets.Property, AllowMultiple=true)]
public class CommandLineSwitchAliasAttribute : Attribute 
{
  // ... 
}

命名参数允许在不提供每个可能属性组合的构造函数的情况下分配属性数据。由于许多属性的属性可能是可选的,因此在许多情况下这是一个有用的构造。

3. FlagsAttribute

FlagsAttribute 是一个框架定义的属性,用于表示枚举类型的值可以组合。以下是使用 FlagsAttribute 的示例:

// FileAttributes defined in System.IO.
[Flags]
public enum FileAttributes 
{
  ReadOnly =          1<<0,      // 000000000000001
  Hidden =            1<<1,      // 000000000000010
  // ... 
}
using System; 
using System.Diagnostics; 
using System.IO;
class Program 
{
  public static void Main()
  {
      string fileName = @"enumtest.txt";
      FileInfo file = new FileInfo(fileName);
      file.Attributes = FileAttributes.Hidden | FileAttributes.ReadOnly;
      Console.WriteLine("\"{0}\" outputs as \"{1}\"",
          file.Attributes.ToString().Replace(",", " |"),
          file.Attributes);
      FileAttributes attributes = 
          (FileAttributes)Enum.Parse(typeof(FileAttributes), 
          file.Attributes.ToString());
      Console.WriteLine(attributes);
  } 
}

输出结果:

"ReadOnly | Hidden" outputs as "ReadOnly, Hidden"

FlagsAttribute 改变了 ToString() Parse() 方法的行为。调用 ToString() 方法时,会输出每个设置的枚举标志的字符串。需要注意的是, FlagsAttribute 不会自动分配唯一的标志值,每个枚举项的值仍必须显式分配。

4. 预定义属性

预定义属性不仅为它们装饰的构造提供额外的元数据,而且运行时和编译器的行为也会有所不同,以促进这些属性的功能。以下是几种常见的预定义属性:

4.1 System.ConditionalAttribute

System.Diagnostics.ConditionalAttribute 类似于 #if/#endif 预处理器标识符,但它不会从程序集中消除 CIL 代码,而是使调用行为像一个空操作。以下是使用示例:

#define CONDITION_A
using System; 
using System.Diagnostics;
public class Program 
{
  public static void Main()
  {
      Console.WriteLine("Begin...");
      MethodA();
      MethodB();
      Console.WriteLine("End...");
  }
  [Conditional("CONDITION_A")]
  static void MethodA()
  {
      Console.WriteLine("MethodA() executing...");
  }
  [Conditional("CONDITION_B")]
  static void MethodB()
  {
      Console.WriteLine("MethodB() executing...");
  } 
}

输出结果:

Begin...
MethodA() executing... 
End...

在这个例子中,由于定义了 CONDITION_A MethodA() 正常执行;而 CONDITION_B 未定义,所有对 Program.MethodB() 的调用都不会执行。

4.2 System.ObsoleteAttribute

ObsoleteAttribute 用于帮助代码的版本控制,向调用者表明某个成员或类型不再是最新的。以下是使用示例:

class Program 
{
  public static void Main()
  {
      ObsoleteMethod();
  }
  [Obsolete]
  public static void ObsoleteMethod()
  {
  } 
}

当编译调用标记有 ObsoleteAttribute 的成员的代码时,会产生编译时警告,也可以通过构造函数参数将警告设置为错误。

5. 序列化相关属性

使用预定义属性,框架支持将对象序列化到流中,以便在以后的时间将其反序列化回对象。这为在关闭应用程序之前将文档类型的对象保存到磁盘提供了一种简单的方法。

5.1 System.SerializableAttribute

为了使对象可序列化,唯一的要求是它包含 System.SerializableAttribute 。以下是保存文档的示例:

using System; 
using System.IO; 
using System.Runtime.Serialization.Formatters.Binary;
class Program 
{
  public static void Main()
  {
      Stream stream;
      Document documentBefore = new Document();
      documentBefore.Title = 
          "A cacophony of ramblings from my potpourri of notes";
      Document documentAfter;
      using (stream = File.Open(
          documentBefore.Title + ".bin", FileMode.Create))
      {
          BinaryFormatter formatter = new BinaryFormatter();
          formatter.Serialize(stream, documentBefore);
      }
      using (stream = File.Open(
          documentBefore.Title + ".bin", FileMode.Open))
      {
          BinaryFormatter formatter = new BinaryFormatter();
          documentAfter = (Document)formatter.Deserialize(stream);
      }
      Console.WriteLine(documentAfter.Title);
  } 
}
// Serializable classes use SerializableAttribute.
[Serializable]
class Document 
{
  public string Title = null;
  public string Data = null;
  public long _WindowHandle = 0;
  [NonSerialized]
  class Image
  {
  }
  private Image Picture = new Image(); 
}

输出结果:

A cacophony of ramblings from my potpourri of notes

序列化涉及实例化一个格式化器(如 BinaryFormatter )并调用 Serialize() 方法,反序列化则调用 Deserialize() 方法。需要注意的是,整个对象图中的所有字段都必须是可序列化的。

5.2 System.NonSerializable

对于不可序列化的字段,应使用 System.NonSerializable 属性进行装饰,以告诉序列化框架忽略它们。例如,密码和 Windows 句柄等字段不应该被序列化。

5.3 自定义序列化

为了添加加密功能,可以提供自定义序列化。这需要实现 ISerializable 接口,并在序列化和反序列化之前进行加密和解密操作。以下是实现示例:

using System; 
using System.Runtime.Serialization;
[Serializable] 
class EncryptableDocument : 
    ISerializable 
{
  public EncryptableDocument(){ }
  enum Field
  {
      Title,
      Data
  }
  public string Title;
  public string Data;
  public static string Encrypt(string data)
  {
      string encryptedData = data;
      // Key-based encryption . . .
      return encryptedData;
  }

  public static string Decrypt(string encryptedData)
  {
      string data = encryptedData;
      // Key-based decryption. . .
      return data;
  }
  #region ISerializable Members
  public void GetObjectData(
      SerializationInfo info, StreamingContext context)
  {
      info.AddValue(
          Field.Title.ToString(), Title);
      info.AddValue(
          Field.Data.ToString(), Encrypt(Data));
  }
  public EncryptableDocument(
      SerializationInfo info, StreamingContext context)
  {
      Title = info.GetString(
          Field.Title.ToString());
      Data = Decrypt(info.GetString(
          Field.Data.ToString()));
  } 
  #endregion 
}
5.4 序列化版本控制

对象可能使用一个版本的程序集进行序列化,并使用较新的版本进行反序列化,这可能会引入版本不兼容问题。例如,仅仅添加一个新字段,反序列化原始文件可能会抛出 System.Runtime.Serialization.SerializationException

以下是一个版本不兼容的示例:
| 步骤 | 描述 | 代码 |
| ---- | ---- | ---- |
| 1 | 定义一个用 System.SerializableAttribute 装饰的类 | [Serializable] class Document { public string Title; public string Data; } |
| 2 | 添加一两个可序列化类型的字段 | |
| 3 | 将对象序列化为名为 *.v1.bin 的文件 | Stream stream; Document documentBefore = new Document(); documentBefore.Title = "A cacophony of ramblings from my potpourri of notes"; Document documentAfter; using (stream = File.Open(documentBefore.Title + ".bin", FileMode.Create)) { BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(stream, documentBefore); } |
| 4 | 向可序列化类中添加一个额外的字段 | [Serializable] class Document { public string Title; public string Author; public string Data; } |
| 5 | 将 *v1.bin 文件反序列化为新的对象版本 | using (stream = File.Open(documentBefore.Title + ".bin", FileMode.Open)) { BinaryFormatter formatter = new BinaryFormatter(); documentAfter = (Document)formatter.Deserialize(stream); } |

为了避免版本不兼容问题,.NET 2.0 及更高版本引入了 System.Runtime.Serialization.OptionalFieldAttribute 。在需要向后兼容时,必须使用该属性装饰序列化字段。对于早期框架版本,需要实现 ISerializable 接口来处理版本兼容性问题。

通过本文的介绍,我们深入了解了 C# 中属性的使用,包括自定义属性的限制、预定义属性的特性以及序列化相关属性的应用。这些知识可以帮助我们更好地编写高质量、可维护的代码。

C# 中的属性与序列化:深入解析与实践

6. 序列化过程中的注意事项及解决方案
6.1 字段的序列化与反序列化

在序列化过程中,需要注意对象图中所有字段的可序列化性。如前面所述,若字段不可序列化,需使用 System.NonSerializable 属性进行标记。而在反序列化时,要确保能正确处理可能缺失的字段。例如,在版本更新后添加了新字段,旧版本序列化的数据中不会包含这些新字段,反序列化时就需要特殊处理。

以下是一个处理可能缺失字段的示例:

using System;
using System.Runtime.Serialization;

[Serializable]
class Document
{
    public string Title;
    public string Data;
    public string Author;

    public Document() { }

    protected Document(SerializationInfo info, StreamingContext context)
    {
        Title = info.GetString("Title");
        Data = info.GetString("Data");

        try
        {
            Author = info.GetString("Author");
        }
        catch (SerializationException)
        {
            // 若旧版本数据中没有 Author 字段,此处可进行默认处理
            Author = "Unknown";
        }
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("Title", Title);
        info.AddValue("Data", Data);
        info.AddValue("Author", Author);
    }
}
6.2 自定义序列化的详细流程

自定义序列化主要用于实现一些特殊需求,如加密、自定义数据格式等。下面详细说明实现自定义序列化的步骤:
1. 实现 ISerializable 接口 :该接口要求实现 GetObjectData 方法和一个特殊的构造函数。
2. GetObjectData 方法 :在序列化时调用,用于将对象的状态保存到 SerializationInfo 对象中。
3. 特殊构造函数 :在反序列化时调用,用于从 SerializationInfo 对象中恢复对象的状态。

以下是一个完整的自定义序列化示例,包含加密和解密操作:

using System;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Text;

[Serializable]
class EncryptedDocument : ISerializable
{
    public string Title;
    public string Data;

    public EncryptedDocument() { }

    protected EncryptedDocument(SerializationInfo info, StreamingContext context)
    {
        Title = Decrypt(info.GetString("Title"));
        Data = Decrypt(info.GetString("Data"));
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("Title", Encrypt(Title));
        info.AddValue("Data", Encrypt(Data));
    }

    private static string Encrypt(string data)
    {
        using (Aes aesAlg = Aes.Create())
        {
            byte[] encrypted;
            using (ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV))
            {
                byte[] dataBytes = Encoding.UTF8.GetBytes(data);
                encrypted = encryptor.TransformFinalBlock(dataBytes, 0, dataBytes.Length);
            }
            return Convert.ToBase64String(encrypted);
        }
    }

    private static string Decrypt(string encryptedData)
    {
        using (Aes aesAlg = Aes.Create())
        {
            byte[] cipherBytes = Convert.FromBase64String(encryptedData);
            byte[] decrypted;
            using (ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV))
            {
                decrypted = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
            }
            return Encoding.UTF8.GetString(decrypted);
        }
    }
}
7. 预定义属性的应用场景总结

预定义属性在不同的场景中发挥着重要作用,下面总结几种常见预定义属性的应用场景:
| 属性名称 | 应用场景 |
| ---- | ---- |
| System.AttributeUsageAttribute | 限制自定义属性的使用范围,确保属性只能应用于特定的构造,如属性、字段、方法等,提高代码的安全性和可读性。 |
| System.ConditionalAttribute | 在调试和测试场景中非常有用,可根据预处理器标识符选择性地执行方法,避免在发布版本中执行不必要的代码。 |
| System.ObsoleteAttribute | 用于代码的版本控制,向开发者提示某个成员或类型已过时,引导他们使用新的替代方案。 |
| System.SerializableAttribute | 使对象能够被序列化和反序列化,方便数据的持久化和传输。 |
| System.NonSerializable | 标记不可序列化的字段,避免在序列化过程中出现错误。 |

8. 总结与建议

通过对 C# 中属性和序列化相关知识的深入学习,我们可以更好地利用这些特性来编写高质量的代码。以下是一些总结和建议:
- 属性的使用 :合理使用 AttributeUsageAttribute 限制自定义属性的使用范围,避免属性的滥用。使用命名参数可以更灵活地配置属性的行为。
- 序列化 :对于需要序列化的对象,确保所有字段都是可序列化的,对于不可序列化的字段使用 System.NonSerializable 进行标记。在处理版本兼容性问题时,可使用 OptionalFieldAttribute 或实现 ISerializable 接口。
- 预定义属性 :根据不同的应用场景选择合适的预定义属性,如在调试时使用 ConditionalAttribute ,在代码版本控制时使用 ObsoleteAttribute

mermaid 流程图:序列化与反序列化流程

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px

    A([开始]):::startend --> B{对象是否可序列化?}:::decision
    B -->|是| C(实例化格式化器):::process
    B -->|否| D(添加 SerializableAttribute):::process
    D --> C
    C --> E(调用 Serialize 方法):::process
    E --> F(数据保存到流中):::process
    F --> G([结束序列化]):::startend
    G --> H([开始反序列化]):::startend
    H --> I(实例化格式化器):::process
    I --> J(调用 Deserialize 方法):::process
    J --> K(从流中恢复对象):::process
    K --> L([结束反序列化]):::startend

总之,掌握 C# 中的属性和序列化知识,能够帮助我们更好地处理数据的持久化、版本控制和代码的可维护性,为开发高质量的应用程序提供有力支持。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值