簡體   English   中英

如何將屬性從一個 object 復制到另一個具有不同值 C#

[英]how to copy properties from one object to another with different values C#

我想將給定 object ClassA中的 Properties 值復制到另一個名為ClassB的 object 實例中,這些類可能是也可能不是同一類型。

如果ClassB中的屬性有值,而ClassA中對應的屬性值為 null,則不要復制該值,因此僅復制ClassB中的當前屬性為 null 的位置。

這不是克隆練習,目標 object ( ClassB ) 已經用部分定義的值實例化,我正在尋找一種可重用的方法來復制尚未設置的值的 rest。

想想我們有一個通用或默認測試數據值的測試場景,對於特定的測試我想設置一些特定的字段,然后從通用測試數據 object 中完成設置其他屬性。

我想我正在尋找一個基於反射的解決方案,因為這樣我們就不需要知道要復制的特定類型,這將使它可以在許多不同的場景中重用。

例如。

public class Employee
{
    public int EmployeeID { get; set; }
    public string EmployeeName { get; set; }
    public Address ContactAddress { get; set; }
}

public class Address
{
    public string Address1 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
}

測試例如。

public void TestMethod1()
{
    Employee employee = new Employee();
    employee.EmployeeID = 100;
    employee.EmployeeName = "John";
    employee.ContactAddress = new Address();
    employee.ContactAddress.Address1 = "Park Ave";
    employee.ContactAddress.City = "New York";
    employee.ContactAddress.State = "NewYork";
    employee.ContactAddress.ZipCode = "10002";
 
    Employee employeeCopy = new Employee();
    employeeCopy.EmployeeID = 101;
    employeeCopy.EmployeeName = "Tom";
    employeeCopy.ContactAddress = new Address();

    CopyPropertiesTo(employee, employeeCopy);
}

我想得到結果

員工復制員工ID = 101;
員工姓名="湯姆";
ContactAddress.Address1 = "公園大道";
ContactAddress.City = "紐約";
ContactAddress.State = "紐約";
ContactAddress.ZipCode = "10002"

因此,在這種情況下,由於沒有設置employeeCopy.ContactAddress中的任何字段,因此只應復制原始employee object 中的那些字段。

我不知道如何編寫方法:
CopyPropertiesTo(object sourceObject, object targetObject)

一種方法是簡單地檢查“to” Employee中的每個屬性,如果它是null0 ,則為其分配來自“from” Employee的值:

/// <summary>
/// Copies values in 'from' to 'to' if they are null in 'to'
/// </summary>
public static void CopyProperties(Employee from, Employee to)
{
    if (from == null) return;
    if (to == null) to = new Employee();

    if (to.EmployeeID == 0) to.EmployeeID = from.EmployeeID;
    if (to.EmployeeName == null) to.EmployeeName = from.EmployeeName;

    if (from.ContactAddress == null) return;
    if (to.ContactAddress == null) to.ContactAddress = new Address();

    if (to.ContactAddress.Address1 == null)
        to.ContactAddress.Address1 = from.ContactAddress.Address1;
    if (to.ContactAddress.City == null)
        to.ContactAddress.City = from.ContactAddress.City;
    if (to.ContactAddress.State == null)
        to.ContactAddress.State = from.ContactAddress.State;
    if (to.ContactAddress.ZipCode == null)
        to.ContactAddress.ZipCode = from.ContactAddress.ZipCode;
}
public static void CopyPropertiesTo(Employee EP1, Employee EP2){
    
    Type eType=typeof(Employee);
    PropertyInfo[] eProps = eType.GetProperties();

    foreach(var p in eProps){
        if(p.PropertyType != typeof(String) && p.PropertyType != typeof(Int32)){
            //Merging Contact Address
            Type cType=p.PropertyType;
            PropertyInfo[] cProps = cType.GetProperties();
            
            foreach(var c in cProps){
                //Check if value is null
                if (String.IsNullOrEmpty((EP2.ContactAddress.GetType().GetProperty(c.Name).GetValue(EP2.ContactAddress) as string))){
                    //Assign Source to Target
                    EP2.ContactAddress.GetType().GetProperty(c.Name).SetValue(EP2.ContactAddress, (EP1.ContactAddress.GetType().GetProperty(c.Name).GetValue(EP1.ContactAddress)));
                }
            }
        }
        else{
            //Check if value is null or empty
            if (String.IsNullOrEmpty((EP2.GetType().GetProperty(p.Name).GetValue(EP2) as string))){
                //Assign Source to Target
                EP2.GetType().GetProperty(p.Name).SetValue(EP2, (EP1.GetType().GetProperty(p.Name).GetValue(EP1)));
            }
        }
    }
}

不是最漂亮的,但這應該可以讓您更改 class 中的屬性名稱/數量。 我從來沒有真正嘗試過這樣做,所以如果有人有一些反饋,我將不勝感激

查看以下鏈接以獲取更多信息和示例PropertyInfo GetType GetProperty

如果還不算太晚,這也是我的建議,但也許會有所幫助。

    public class Source
    {
        [DefaultValueAttribute(-1)]
        public int Property { get; set; }

        public int AnotherProperty { get; set; }
    }

    public class Dedstination
    {
        public int Property { get; set; }

        [DefaultValueAttribute(42)]
        public int AnotherProperty { get; set; }
    }

    public void Main()
    {
        var source = new Source { Property = 10, AnotherProperty = 76 };
        var destination = new Dedstination();

        MapValues(source, destination);
    }

    public static void MapValues<TS, TD>(TS source, TD destination)
    {
        var srcPropsWithValues = typeof(TS)
            .GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .ToDictionary(x => x.Name, y => y.GetValue(source));

        var dstProps = typeof(TD)
       .GetProperties(BindingFlags.Public | BindingFlags.Instance)
       .ToDictionary(key => key, value => value.GetCustomAttribute<DefaultValueAttribute>()?.Value
                                       ?? (value.PropertyType.IsValueType
                                       ? Activator.CreateInstance(value.PropertyType, null)
                                       : null));

        foreach (var prop in dstProps)
        {
            var destProperty = prop.Key;

            if (srcPropsWithValues.ContainsKey(destProperty.Name))
            {
                var defaultValue = prop.Value;
                var currentValue = destProperty.GetValue(destination);
                var sourceValue = srcPropsWithValues[destProperty.Name];

                if (currentValue.Equals(defaultValue) && !sourceValue.Equals(defaultValue))
                {
                    destProperty.SetValue(destination, sourceValue);
                }
            }
        }
    }

編輯:我編輯了我的解決方案,以消除對使用 DefaultValueAttribute 的依賴。 現在,您可以從指定的屬性或類型默認值中獲取默認值。

以前的解決方案如下:

        // This solution do not needs DefaultValueAttributes 
        var dstProps = typeof(TD)
           .GetProperties(BindingFlags.Public | BindingFlags.Instance)
           .ToDictionary(x => x, x => x.PropertyType.IsValueType ? Activator.CreateInstance(x.PropertyType, null) : null);

        // This solution needs DefaultValueAttributes 
        var dstProps = typeof(TD)
           .GetProperties(BindingFlags.Public | BindingFlags.Instance)
           .ToDictionary(x => x, x => x.GetCustomAttribute<DefaultValueAttribute>()?.Value ?? null);

在復制完成和/或實現 ICloneable 接口后對新實例進行更改。 https://docs.microsoft.com/en-us/dotnet/api/system.icloneable?view=netcore-3.1

深度克隆可以通過序列化輕松實現,但是僅跨非空字段復制需要更多條件邏輯,在這種情況下,我將其稱為Coalesce ,因此我將我的方法命名為CoalesceTo 如果您願意,您可以將其重構為擴展方法,但我不推薦它,而是將其放在 static 助手 class 中。 盡管這可能很有用,但我不鼓勵將其作為生產業務運行時的“goto”。

對這些類型的解決方案使用反射通常是效率最低的機制,但它為我們提供了很大的靈活性,並且非常適合 mocking、原型設計和快速單元測試表達式。

  • 盡管不在此示例中,但可以輕松添加檢查以排除高級場景的[Obsolete]屬性

以下示例使用屬性名稱比較,因此您不必傳入相同類型的對象。 請注意,已創建IsNullIsValueType方法來封裝這些概念,從而簡化您可能希望對此方法進行的調整。

  • 此方法還在繼續之前檢查是否可以讀取/寫入屬性,這允許我們在源 object 上支持只讀屬性,當然我們不會嘗試寫入只讀屬性。
  • 解析和寫入的最終值包含在抑制任何錯誤的 try catch 語句中,需要進行一些調整才能使這樣的代碼普遍工作,但對於簡單的類型定義應該可以正常工作。
/// <summary>
/// Deep Copy the top level properties from this object only if the corresponding property on the target object IS NULL.
/// </summary>
/// <param name="source">the source object to copy from</param>
/// <param name="target">the target object to update</param>
/// <returns>A reference to the Target instance for chaining, no changes to this instance.</returns>
public static void CoalesceTo(object source, object target, StringComparison propertyComparison = StringComparison.OrdinalIgnoreCase)
{
    var sourceType = source.GetType();
    var targetType = target.GetType();
    var targetProperties = targetType.GetProperties();
    foreach(var sourceProp in sourceType.GetProperties())
    {
        if(sourceProp.CanRead)
        {
            var sourceValue = sourceProp.GetValue(source);

            // Don't copy across nulls or defaults
            if (!IsNull(sourceValue, sourceProp.PropertyType))
            {
                var targetProp = targetProperties.FirstOrDefault(x => x.Name.Equals(sourceProp.Name, propertyComparison));
                if (targetProp != null && targetProp.CanWrite)
                {
                    if (!targetProp.CanRead)
                        continue; // special case, if we cannot verify the destination, assume it has a value.
                    else if (targetProp.PropertyType.IsArray || targetProp.PropertyType.IsGenericType // It is ICollection<T> or IEnumerable<T>
                                                                && targetProp.PropertyType.GenericTypeArguments.Any()
                                                                && targetProp.PropertyType.GetGenericTypeDefinition() != typeof(Nullable<>) // because that will also resolve GetElementType!
                            )
                        continue; // special case, skip arrays and collections...
                    else
                    {
                        // You can do better than this, for now if conversion fails, just skip it
                        try
                        {
                            var existingValue = targetProp.GetValue(target);
                            if (IsValueType(targetProp.PropertyType))
                            {
                                // check that the destination is NOT already set.
                                if (IsNull(existingValue, targetProp.PropertyType))
                                {
                                    // we do not overwrite a non-null destination value
                                    object targetValue = sourceValue;
                                    if (!targetProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
                                    {
                                        // TODO: handle specific types that don't go across.... or try some brute force type conversions if neccessary
                                        if (targetProp.PropertyType == typeof(string))
                                            targetValue = targetValue.ToString();
                                        else 
                                            targetValue = Convert.ChangeType(targetValue, targetProp.PropertyType);
                                    }

                                    targetProp.SetValue(target, targetValue);
                                }
                            }
                            else if (!IsValueType(sourceProp.PropertyType))
                            {
                                // deep clone
                                if (existingValue == null)
                                    existingValue = Activator.CreateInstance(targetProp.PropertyType);

                                CoalesceTo(sourceValue, existingValue);
                            }
                        }
                        catch (Exception)
                        {
                            // suppress exceptions, don't set a field that we can't set
                        }

                    }
                }
            }
        }
    }
}

/// <summary>
/// Check if a boxed value is null or not
/// </summary>
/// <remarks>
/// Evaluate your own logic or definition of null in here.
/// </remarks>
/// <param name="value">Value to inspect</param>
/// <param name="valueType">Type of the value, pass it in if you have it, otherwise it will be resolved through reflection</param>
/// <returns>True if the value is null or primitive default, otherwise False</returns>
public static bool IsNull(object value, Type valueType = null)
{
    if (value is null)
        return true;

    if (valueType == null) valueType = value.GetType();

    if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType)
    {
        // Handle nullable types like float? or Nullable<Int>
        if (valueType.IsGenericType)
            return value is null;
        else
            return Activator.CreateInstance(valueType).Equals(value);
    }

    // treat empty string as null!
    if (value is string s)
        return String.IsNullOrWhiteSpace(s);

    return false;
}
/// <summary>
/// Check if a type should be copied by value or if it is a complexe type that should be deep cloned
/// </summary>
/// <remarks>
/// Evaluate your own logic or definition of Object vs Value/Primitive here.
/// </remarks>
/// <param name="valueType">Type of the value to check</param>
/// <returns>True if values of this type can be straight copied, false if they should be deep cloned</returns>
public static bool IsValueType(Type valueType)
{
    // TODO: any specific business types that you want to treat as value types?

    // Standard .Net Types that can be treated as value types
    if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType || valueType == typeof(string))
        return true;

    // Support Nullable Types as Value types (Type.IsValueType) should deal with this, but just in case
    if (valueType.HasElementType // It is array/enumerable/nullable
        && valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(Nullable<>))
        return true;


    return false;
}

因為我們在這里使用反射,所以我們無法利用Generics可以為我們提供的優化。 如果您想將其應用於生產環境,請考慮使用 T4 模板來編寫此邏輯的通用類型版本作為您的業務類型的擴展方法。

深度克隆 -

你會注意到我特別跳過了 arrays 和其他 IEnumerable 結構......有一大堆蠕蟲在支持它們,最好不要讓一種方法嘗試深度復制,所以把嵌套調用CoalesceTo拿出來,然后在樹中的每個 object 上調用 clone 方法。

數組/集合/列表的問題在於,在克隆之前,您需要確定一種將源中的集合與目標中的集合同步的方法,您可以根據 Id 字段或某種形式進行約定像[KeyAttribute]這樣的屬性,但這種實現需要高度特定於您的業務邏輯,並且不在這個已經很可怕的帖子的 scope 之外;)

DecimalDateTime這樣的類型在這些類型的場景中是有問題的,它們不應該與 null 進行比較,而是我們必須將它們與它們的默認類型狀態進行比較,同樣我們不能在這種情況下使用通用的default運算符或值,因為類型只能在運行時解析。

因此,我更改了您的類,以包含一個示例,說明此邏輯如何處理 DateTimeOffset:

public class Employee
{
    public int EmployeeID { get; set; }
    public string EmployeeName { get; set; }
    public DateTimeOffset Date { get; set; }
    public float? Capacity { get; set; }
    Nullable<int> MaxShift { get; set; }
    public Address ContactAddress { get; set; }
}

public class Address
{
    public string Address1 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
}

public static  void TestMethod1()
{
    Employee employee = new Employee();
    employee.EmployeeID = 100;
    employee.EmployeeName = "John";
    employee.Capacity = 26.2f;
    employee.MaxShift = 8;
    employee.Date = new DateTime(2020,1,22);
    employee.ContactAddress = new Address();
    employee.ContactAddress.Address1 = "Park Ave";
    employee.ContactAddress.City = "New York";
    employee.ContactAddress.State = "NewYork";
    employee.ContactAddress.ZipCode = "10002";

    Employee employeeCopy = new Employee();
    employeeCopy.EmployeeID = 101;
    employeeCopy.EmployeeName = "Tom";
    employeeCopy.ContactAddress = new Address();

    CoalesceTo(employee, employeeCopy);
}

這會產生以下 object 圖:

{
  "EmployeeID": 101,
  "EmployeeName": "Tom",
  "Date": "2020-01-22T00:00:00+11:00",
  "Capacity":26.2,
  "MaxShift":8,
  "ContactAddress": {
    "Address1": "Park Ave",
    "City": "New York",
    "State": "NewYork",
    "ZipCode": "10002"
  }
}

如果您首先進行完整的深度克隆然后設置您的值,那么這些類型的問題通常會更容易且資源消耗更少,而不是嘗試進行深度復制

SO上有很多關於深度克隆的帖子,我的偏好只是使用 JSON.Net 進行序列化然后反序列化。

 public static T Clone<T>(T value, Newtonsoft.Json.JsonSerializerSettings settings = null) { var objectType = value.GetType(); var cereal = Newtonsoft.Json.JsonConvert.SerializeObject(value, settings); return (T)Newtonsoft.Json.JsonConvert.DeserializeObject(cereal, objectType, settings); }

但是,此代碼需要Newtonsoft.Json nuget package 參考。

克隆 object首先設置所有通用/默認值,然后我們只修改此特定測試或代碼塊所需的那些屬性。

public void TestMethod1()
{
    Employee employee = new Employee();
    employee.EmployeeID = 100;
    employee.EmployeeName = "John";
    employee.ContactAddress = new Address();
    employee.ContactAddress.Address1 = "Park Ave";
    employee.ContactAddress.City = "New York";
    employee.ContactAddress.State = "NewYork";
    employee.ContactAddress.ZipCode = "10002";
 
    // Create a deep clone of employee
    Employee employeeCopy = Clone(employee);

    // set the specific fields that we want to change
    employeeCopy.EmployeeID = 101;
    employeeCopy.EmployeeName = "Tom";

}

如果我們願意改變我們的方法,我們通常可以找到更簡單的解決方案,這個解決方案將具有相同的 output,就好像我們有條件地復制了屬性值一樣,但沒有進行任何比較。

如果您有條件副本的其他原因,在這篇文章的其他解決方案中稱為MergeCoalesce ,那么我使用反射的其他答案將完成這項工作,但它不如這個強大。

[TestClass]
public class UnitTest11
{
    [TestMethod]
    public void TestMethod1()
    {

        Employee employee = new Employee();
        employee.EmployeeID = 100;
        employee.EmployeeName = "John";
        employee.Date = DateTime.Now;
        employee.ContactAddress = new Address();
        employee.ContactAddress.Address1 = "Park Ave";
        employee.ContactAddress.City = "New York";
        employee.ContactAddress.State = "NewYork";
        employee.ContactAddress.ZipCode = "10002";

        Employee employeeCopy = new Employee();
        employeeCopy.EmployeeID = 101;
        employeeCopy.EmployeeName = "Tom";
        employeeCopy.ContactAddress = new Address();
        employeeCopy.ContactAddress.City = "Bei Jing";
        //copy all properties from employee to employeeCopy
        CoalesceTo(employee, employeeCopy);

        Console.ReadLine();
    }

    /// Deep Copy the top level properties from this object only if the corresponding property on the target object IS NULL.
    /// </summary>
    /// <param name="source">the source object to copy from</param>
    /// <param name="target">the target object to update</param>
    /// <returns>A reference to the Target instance for chaining, no changes to this instance.</returns>
    public static void CoalesceTo(object source, object target, StringComparison propertyComparison = StringComparison.OrdinalIgnoreCase)
    {
        var sourceType = source.GetType();
        var targetType = target.GetType();
        var targetProperties = targetType.GetProperties();
        foreach (var sourceProp in sourceType.GetProperties())
        {
            if (sourceProp.CanRead)
            {
                var sourceValue = sourceProp.GetValue(source);

                // Don't copy across nulls or defaults
                if (!IsNull(sourceValue, sourceProp.PropertyType))
                {
                    var targetProp = targetProperties.FirstOrDefault(x => x.Name.Equals(sourceProp.Name, propertyComparison));
                    if (targetProp != null && targetProp.CanWrite)
                    {
                        if (!targetProp.CanRead)
                            continue; // special case, if we cannot verify the destination, assume it has a value.
                        else if (targetProp.PropertyType.IsArray || targetProp.PropertyType.IsGenericType // It is ICollection<T> or IEnumerable<T>
                                                                    && targetProp.PropertyType.GenericTypeArguments.Any()
                                                                    && targetProp.PropertyType.GetGenericTypeDefinition() != typeof(Nullable<>) // because that will also resolve GetElementType!
                                )
                            continue; // special case, skip arrays and collections...
                        else
                        {
                            // You can do better than this, for now if conversion fails, just skip it
                            try
                            {
                                var existingValue = targetProp.GetValue(target);
                                if (IsValueType(targetProp.PropertyType))
                                {
                                    // check that the destination is NOT already set.
                                    if (IsNull(existingValue, targetProp.PropertyType))
                                    {
                                        // we do not overwrite a non-null destination value
                                        object targetValue = sourceValue;
                                        if (!targetProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
                                        {
                                            // TODO: handle specific types that don't go across.... or try some brute force type conversions if neccessary
                                            if (targetProp.PropertyType == typeof(string))
                                                targetValue = targetValue.ToString();
                                            else
                                                targetValue = Convert.ChangeType(targetValue, targetProp.PropertyType);
                                        }

                                        targetProp.SetValue(target, targetValue);
                                    }
                                }
                                else if (!IsValueType(sourceProp.PropertyType))
                                {
                                    // deep clone
                                    if (existingValue == null)
                                        existingValue = Activator.CreateInstance(targetProp.PropertyType);

                                    CoalesceTo(sourceValue, existingValue);
                                }
                            }
                            catch (Exception)
                            {
                                // suppress exceptions, don't set a field that we can't set
                            }

                        }
                    }
                }
            }
        }
    }

    /// <summary>
    /// Check if a boxed value is null or not
    /// </summary>
    /// <remarks>
    /// Evaluate your own logic or definition of null in here.
    /// </remarks>
    /// <param name="value">Value to inspect</param>
    /// <param name="valueType">Type of the value, pass it in if you have it, otherwise it will be resolved through reflection</param>
    /// <returns>True if the value is null or primitive default, otherwise False</returns>
    public static bool IsNull(object value, Type valueType = null)
    {
        if (value is null)
            return true;

        if (valueType == null) valueType = value.GetType();

        if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType)
            return value.Equals(Activator.CreateInstance(valueType));

        // treat empty string as null!
        if (value is string s)
            return String.IsNullOrWhiteSpace(s);

        return false;
    }
    /// <summary>
    /// Check if a type should be copied by value or if it is a complexe type that should be deep cloned
    /// </summary>
    /// <remarks>
    /// Evaluate your own logic or definition of Object vs Value/Primitive here.
    /// </remarks>
    /// <param name="valueType">Type of the value to check</param>
    /// <returns>True if values of this type can be straight copied, false if they should be deep cloned</returns>
    public static bool IsValueType(Type valueType)
    {
        // TODO: any specific business types that you want to treat as value types?

        // Standard .Net Types that can be treated as value types
        if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType || valueType == typeof(string))
            return true;

        // Support Nullable Types as Value types (Type.IsValueType) should deal with this, but just in case
        if (valueType.HasElementType // It is array/enumerable/nullable
            && valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(Nullable<>))
            return true;


        return false;
    }
}


public class Employee
{
    public int EmployeeID { get; set; }
    public string EmployeeName { get; set; }
    public DateTimeOffset Date { get; set; }
    public float? check { get; set; }
    public Address ContactAddress { get; set; }
}

public class Address
{
    public string Address1 { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
}

非常感謝大家,尤其是@Chris Schaller,我在上面發布了代碼

private Employee Check(Employee employee,Employee employeeCopy)
        {
if (employeeCopy.EmployeeID==0 && employee.EmployeeID !=0)
  {
     employeeCopy.EmployeeID = employee.EmployeeID;
  }
if (employeeCopy.EmployeeName == null && employee.EmployeeName != null)
  {
     employeeCopy.EmployeeName = employee.EmployeeName;
  }
if (employeeCopy.ContactAddress == null)
{
if (employeeCopy.ContactAddress.Address1 == null && employee.ContactAddress.Address1 != null)
  {
     employeeCopy.ContactAddress.Address1 = employee.ContactAddress.Address1;
  }
if (employeeCopy.ContactAddress.City == null && employee.ContactAddress.City != null)
 {
     employeeCopy.ContactAddress.City = employee.ContactAddress.City;
 }
if (employeeCopy.ContactAddress.State == null && employee.ContactAddress.State != null)
 {
     employeeCopy.ContactAddress.State = employee.ContactAddress.State;
 }
if (employeeCopy.ContactAddress.ZipCode == null && employee.ContactAddress.ZipCode != null)
 {
    employeeCopy.ContactAddress.ZipCode = employee.ContactAddress.ZipCode;
 }
}
            return employeeCopy;

}

這是你想要的?

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM