【问题标题】:Get property value from string using reflection使用反射从字符串中获取属性值
【发布时间】:2010-11-14 21:11:02
【问题描述】:

我正在尝试在我的代码中实现 Data transformation using Reflection1 示例。

GetSourceValue 函数有一个比较各种类型的开关,但我想删除这些类型和属性,并让GetSourceValue 仅使用单个字符串作为参数来获取属性的值。我想在字符串中传递一个类和属性并解析该属性的值。

这可能吗?

1Web Archive version of original blog post

【问题讨论】:

    标签: c# reflection properties


    【解决方案1】:
     public static object GetPropValue(object src, string propName)
     {
         return src.GetType().GetProperty(propName).GetValue(src, null);
     }
    

    当然,你会想要添加验证和诸如此类的东西,但这就是它的要点。

    【讨论】:

    • 又好又简单!不过,我会使其通用:public static T GetPropertyValue<T>(object obj, string propName) { return (T)obj.GetType().GetProperty(propName).GetValue(obj, null); }
    • 优化可以消除空异常的风险,如下所示:“src.GetType().GetProperty(propName)?.GetValue(src, null);”;)。
    • @shA.t:我认为这是个坏主意。您如何区分现有属性的空值或根本没有属性?我宁愿立即知道我发送了一个错误的属性名称。这不是生产代码,但更好的改进是抛出更具体的异常(例如,检查 GetProperty 上的 null 并抛出 PropertyNotFoundException 或如果为 null。)
    • 万一你的属性真的是一个字段而不是一个属性(就像我的;))然后使用GetField而不是GetProperty
    【解决方案2】:

    这样的事情怎么样:

    public static Object GetPropValue(this Object obj, String name) {
        foreach (String part in name.Split('.')) {
            if (obj == null) { return null; }
    
            Type type = obj.GetType();
            PropertyInfo info = type.GetProperty(part);
            if (info == null) { return null; }
    
            obj = info.GetValue(obj, null);
        }
        return obj;
    }
    
    public static T GetPropValue<T>(this Object obj, String name) {
        Object retval = GetPropValue(obj, name);
        if (retval == null) { return default(T); }
    
        // throws InvalidCastException if types are incompatible
        return (T) retval;
    }
    

    这将允许您使用单个字符串进入属性,如下所示:

    DateTime now = DateTime.Now;
    int min = GetPropValue<int>(now, "TimeOfDay.Minutes");
    int hrs = now.GetPropValue<int>("TimeOfDay.Hours");
    

    您可以将这些方法用作静态方法或扩展。

    【讨论】:

    • @FredJand 很高兴你偶然发现了它!当这些旧帖子出现时,总是令人惊讶。这有点模糊,所以我添加了一些文字来解释它。我也切换到使用这些作为扩展方法并添加了一个泛型表单,所以我在这里添加了它。
    • 为什么空守卫在foreach而不是上面?
    • @Santhos 因为 'obj' 在 foreach 循环的主体中被重新定义,所以在每次迭代期间都会对其进行检查。
    • 很有用,但在嵌套属性之一可能被隐藏(使用'new'修饰符)的边缘情况下,它会抛出异常以查找重复属性。跟踪最后一个属性类型并在嵌套属性上使用 PropertyInfo.PropertyType 而不是 obj.GetType() 会更整洁,就像访问嵌套属性上的属性一样。
    • 您可以在 C#6 中使用 nameof 表达式,如下所示:nameof(TimeOfDay.Minutes) 在调用函数以消除魔术字符串并为这些调用添加编译时安全性时的名称参数。
    【解决方案3】:

    添加到任何Class

    public class Foo
    {
        public object this[string propertyName]
        {
            get { return this.GetType().GetProperty(propertyName).GetValue(this, null); }
            set { this.GetType().GetProperty(propertyName).SetValue(this, value, null); }
        }
    
        public string Bar { get; set; }
    }
    

    然后,你可以使用 as:

    Foo f = new Foo();
    // Set
    f["Bar"] = "asdf";
    // Get
    string s = (string)f["Bar"];
    

    【讨论】:

    • @EduardoCuomo:是否可以使用反射,这样您就不需要知道该类有哪些成员?
    • 如果“Bar”是一个对象,是否可以这样做?
    • 这种方法的名称是什么..?
    • 非常感谢,我把这个放在我的一些课上
    【解决方案4】:

    如何使用Microsoft.VisualBasic 命名空间(Microsoft.VisualBasic.dll)的CallByName?它使用反射来获取普通对象、COM 对象甚至动态对象的属性、字段和方法。

    using Microsoft.VisualBasic;
    using Microsoft.VisualBasic.CompilerServices;
    

    然后

    Versioned.CallByName(this, "method/function/prop name", CallType.Get).ToString();
    

    【讨论】:

    • 有趣的建议,进一步检查证明它可以同时处理字段和属性、COM 对象,它甚至可以正确处理动态绑定
    • 我收到一个错误:找不到类型“MyType”的公共成员“MyPropertyName”。
    【解决方案5】:

    Great answer by jheddings。我想通过允许引用聚合数组或对象集合来改进它,以便 propertyName 可以是 property1.property2[X].property3

        public static object GetPropertyValue(object srcobj, string propertyName)
        {
            if (srcobj == null)
                return null;
    
            object obj = srcobj;
    
            // Split property name to parts (propertyName could be hierarchical, like obj.subobj.subobj.property
            string[] propertyNameParts = propertyName.Split('.');
    
            foreach (string propertyNamePart in propertyNameParts)
            {
                if (obj == null)    return null;
    
                // propertyNamePart could contain reference to specific 
                // element (by index) inside a collection
                if (!propertyNamePart.Contains("["))
                {
                    PropertyInfo pi = obj.GetType().GetProperty(propertyNamePart);
                    if (pi == null) return null;
                    obj = pi.GetValue(obj, null);
                }
                else
                {   // propertyNamePart is areference to specific element 
                    // (by index) inside a collection
                    // like AggregatedCollection[123]
                    //   get collection name and element index
                    int indexStart = propertyNamePart.IndexOf("[")+1;
                    string collectionPropertyName = propertyNamePart.Substring(0, indexStart-1);
                    int collectionElementIndex = Int32.Parse(propertyNamePart.Substring(indexStart, propertyNamePart.Length-indexStart-1));
                    //   get collection object
                    PropertyInfo pi = obj.GetType().GetProperty(collectionPropertyName);
                    if (pi == null) return null;
                    object unknownCollection = pi.GetValue(obj, null);
                    //   try to process the collection as array
                    if (unknownCollection.GetType().IsArray)
                    {
                        object[] collectionAsArray = unknownCollection as object[];
                        obj = collectionAsArray[collectionElementIndex];
                    }
                    else
                    {
                        //   try to process the collection as IList
                        System.Collections.IList collectionAsList = unknownCollection as System.Collections.IList;
                        if (collectionAsList != null)
                        {
                            obj = collectionAsList[collectionElementIndex];
                        }
                        else
                        {
                            // ??? Unsupported collection type
                        }
                    }
                }
            }
    
            return obj;
        }
    

    【讨论】:

    • MasterList[0][1] 访问的列表列表怎么样?
    • as Array -> as object[] 也会导致 Nullreference 异常。对我有用的方法(不是最有效的方法)是将 unknownCollection 转换为 IEnumerable ,然后在结果上使用 ToArray() 。 fiddle
    【解决方案6】:

    如果我使用来自Ed S. 的代码,我会得到

    'ReflectionExtensions.GetProperty(Type, string)' 由于其保护级别而无法访问

    似乎 GetProperty() 在 Xamarin.Forms 中不可用。 TargetFrameworkProfile 在我的可移植类库(.NET Framework 4.5、Windows 8、ASP.NET Core 1.0、Xamarin.Android、Xamarin.iOS、Xamarin.iOS Classic)中是 Profile7

    现在我找到了一个可行的解决方案:

    using System.Linq;
    using System.Reflection;
    
    public static object GetPropValue(object source, string propertyName)
    {
        var property = source.GetType().GetRuntimeProperties().FirstOrDefault(p => string.Equals(p.Name, propertyName, StringComparison.OrdinalIgnoreCase));
        return property?.GetValue(source);
    }
    

    Source

    【讨论】:

    • 只是一个微小的改进。替换 IF 和 next return 为:return property?.GetValue(source);
    【解决方案7】:

    .NET Standard 中的调用方法已更改(从 1.6 开始)。我们也可以使用 C# 6 的 null 条件运算符。

    using System.Reflection; 
    public static object GetPropValue(object src, string propName)
    {
        return src.GetType().GetRuntimeProperty(propName)?.GetValue(src);
    }
    

    【讨论】:

    • 准备使用? operator
    【解决方案8】:

    关于嵌套属性的讨论,如果你使用DataBinder.Eval Method (Object, String),你可以避免所有反射的东西,如下所示:

    var value = DataBinder.Eval(DateTime.Now, "TimeOfDay.Hours");
    

    当然,您需要添加对 System.Web 程序集的引用,但这可能没什么大不了的。

    【讨论】:

      【解决方案9】:

      以下方法非常适合我:

      class MyClass {
          public string prop1 { set; get; }
      
          public object this[string propertyName]
          {
              get { return this.GetType().GetProperty(propertyName).GetValue(this, null); }
              set { this.GetType().GetProperty(propertyName).SetValue(this, value, null); }
          }
      }
      

      获取属性值:

      MyClass t1 = new MyClass();
      ...
      string value = t1["prop1"].ToString();
      

      设置属性值:

      t1["prop1"] = value;
      

      【讨论】:

        【解决方案10】:
        public static List<KeyValuePair<string, string>> GetProperties(object item) //where T : class
            {
                var result = new List<KeyValuePair<string, string>>();
                if (item != null)
                {
                    var type = item.GetType();
                    var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
                    foreach (var pi in properties)
                    {
                        var selfValue = type.GetProperty(pi.Name).GetValue(item, null);
                        if (selfValue != null)
                        {
                            result.Add(new KeyValuePair<string, string>(pi.Name, selfValue.ToString()));
                        }
                        else
                        {
                            result.Add(new KeyValuePair<string, string>(pi.Name, null));
                        }
                    }
                }
                return result;
            }
        

        这是一种在列表中获取所有属性及其值的方法。

        【讨论】:

        • 为什么要这样做:type.GetProperty(pi.Name) 当 == 变量 pi 时?
        • 如果你使用c# 6.0,去掉if,改成selfValue?.ToString(),否则去掉if,改用selfValue==null?null:selfValue.ToString()
        • 还有List&lt;KeyValuePair&lt;的列表很奇怪,使用字典Dictionary&lt;string, string&gt;
        【解决方案11】:

        使用 System.Reflection 命名空间的 PropertyInfo。无论我们尝试访问什么属性,反射都能正常编译。该错误将在运行时出现。

            public static object GetObjProperty(object obj, string property)
            {
                Type t = obj.GetType();
                PropertyInfo p = t.GetProperty("Location");
                Point location = (Point)p.GetValue(obj, null);
                return location;
            }
        

        获取对象的 Location 属性效果很好

        Label1.Text = GetObjProperty(button1, "Location").ToString();
        

        我们将获得位置:{X=71,Y=27} 我们也可以用同样的方式返回location.X或者location.Y。

        【讨论】:

          【解决方案12】:

          以下代码是一种递归方法,用于显示对象实例中包含的所有属性名称和值的整个层次结构。此方法在此线程中使用上述 AlexD 的GetPropertyValue() 答案的简化版本。多亏了这个讨论帖,我才知道该怎么做!

          例如,我使用此方法通过调用如下方法来显示WebService 响应中所有属性的爆炸或转储:

          PropertyValues_byRecursion("Response", response, false);

          public static object GetPropertyValue(object srcObj, string propertyName)
          {
            if (srcObj == null) 
            {
              return null; 
            }
            PropertyInfo pi = srcObj.GetType().GetProperty(propertyName.Replace("[]", ""));
            if (pi == null)
            {
              return null;
            }
            return pi.GetValue(srcObj);
          }
          
          public static void PropertyValues_byRecursion(string parentPath, object parentObj, bool showNullValues)
          {
            /// Processes all of the objects contained in the parent object.
            ///   If an object has a Property Value, then the value is written to the Console
            ///   Else if the object is a container, then this method is called recursively
            ///       using the current path and current object as parameters
          
            // Note:  If you do not want to see null values, set showNullValues = false
          
            foreach (PropertyInfo pi in parentObj.GetType().GetTypeInfo().GetProperties())
            {
              // Build the current object property's namespace path.  
              // Recursion extends this to be the property's full namespace path.
              string currentPath = parentPath + "." + pi.Name;
          
              // Get the selected property's value as an object
              object myPropertyValue = GetPropertyValue(parentObj, pi.Name);
              if (myPropertyValue == null)
              {
                // Instance of Property does not exist
                if (showNullValues)
                {
                  Console.WriteLine(currentPath + " = null");
                  // Note: If you are replacing these Console.Write... methods callback methods,
                  //       consider passing DBNull.Value instead of null in any method object parameters.
                }
              }
              else if (myPropertyValue.GetType().IsArray)
              {
                // myPropertyValue is an object instance of an Array of business objects.
                // Initialize an array index variable so we can show NamespacePath[idx] in the results.
                int idx = 0;
                foreach (object business in (Array)myPropertyValue)
                {
                  if (business == null)
                  {
                    // Instance of Property does not exist
                    // Not sure if this is possible in this context.
                    if (showNullValues)
                    {
                      Console.WriteLine(currentPath  + "[" + idx.ToString() + "]" + " = null");
                    }
                  }
                  else if (business.GetType().IsArray)
                  {
                    // myPropertyValue[idx] is another Array!
                    // Let recursion process it.
                    PropertyValues_byRecursion(currentPath + "[" + idx.ToString() + "]", business, showNullValues);
                  }
                  else if (business.GetType().IsSealed)
                  {
                    // Display the Full Property Path and its Value
                    Console.WriteLine(currentPath + "[" + idx.ToString() + "] = " + business.ToString());
                  }
                  else
                  {
                    // Unsealed Type Properties can contain child objects.
                    // Recurse into my property value object to process its properties and child objects.
                    PropertyValues_byRecursion(currentPath + "[" + idx.ToString() + "]", business, showNullValues);
                  }
                  idx++;
                }
              }
              else if (myPropertyValue.GetType().IsSealed)
              {
                // myPropertyValue is a simple value
                Console.WriteLine(currentPath + " = " + myPropertyValue.ToString());
              }
              else
              {
                // Unsealed Type Properties can contain child objects.
                // Recurse into my property value object to process its properties and child objects.
                PropertyValues_byRecursion(currentPath, myPropertyValue, showNullValues);
              }
            }
          }
          

          【讨论】:

            【解决方案13】:
            public static TValue GetFieldValue<TValue>(this object instance, string name)
            {
                var type = instance.GetType(); 
                var field = type.GetFields(BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance).FirstOrDefault(e => typeof(TValue).IsAssignableFrom(e.FieldType) && e.Name == name);
                return (TValue)field?.GetValue(instance);
            }
            
            public static TValue GetPropertyValue<TValue>(this object instance, string name)
            {
                var type = instance.GetType();
                var field = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance).FirstOrDefault(e => typeof(TValue).IsAssignableFrom(e.PropertyType) && e.Name == name);
                return (TValue)field?.GetValue(instance);
            }
            

            【讨论】:

              【解决方案14】:
              public class YourClass
              {
                  //Add below line in your class
                  public object this[string propertyName] => GetType().GetProperty(propertyName)?.GetValue(this, null);
                  public string SampleProperty { get; set; }
              }
              
              //And you can get value of any property like this.
              var value = YourClass["SampleProperty"];
              

              【讨论】:

                【解决方案15】:
                Dim NewHandle As YourType = CType(Microsoft.VisualBasic.CallByName(ObjectThatContainsYourVariable, "YourVariableName", CallType), YourType)
                

                【讨论】:

                  【解决方案16】:

                  这是另一种查找嵌套属性的方法,该方法不需要字符串告诉您嵌套路径。感谢 Ed S. 的单一属性方法。

                      public static T FindNestedPropertyValue<T, N>(N model, string propName) {
                          T retVal = default(T);
                          bool found = false;
                  
                          PropertyInfo[] properties = typeof(N).GetProperties();
                  
                          foreach (PropertyInfo property in properties) {
                              var currentProperty = property.GetValue(model, null);
                  
                              if (!found) {
                                  try {
                                      retVal = GetPropValue<T>(currentProperty, propName);
                                      found = true;
                                  } catch { }
                              }
                          }
                  
                          if (!found) {
                              throw new Exception("Unable to find property: " + propName);
                          }
                  
                          return retVal;
                      }
                  
                          public static T GetPropValue<T>(object srcObject, string propName) {
                          return (T)srcObject.GetType().GetProperty(propName).GetValue(srcObject, null);
                      }
                  

                  【讨论】:

                  • 最好检查Type.GetProperty 是否返回null 而不是调用GetValue 并将NullReferenceExceptions 抛出一个循环。
                  【解决方案17】:

                  您从不提及您正在检查的对象,并且由于您拒绝引用给定对象的对象,因此我假设您的意思是静态对象。

                  using System.Reflection;
                  public object GetPropValue(string prop)
                  {
                      int splitPoint = prop.LastIndexOf('.');
                      Type type = Assembly.GetEntryAssembly().GetType(prop.Substring(0, splitPoint));
                      object obj = null;
                      return type.GetProperty(prop.Substring(splitPoint + 1)).GetValue(obj, null);
                  }
                  

                  请注意,我用局部变量obj 标记了正在检查的对象。 null 表示静态,否则设置为您想要的。另请注意,GetEntryAssembly() 是获取“正在运行”程序集的几种可用方法之一,如果您在加载类型时遇到困难,您可能想尝试一下。

                  【讨论】:

                    【解决方案18】:

                    看看Heleonix.Reflection 库。您可以通过路径获取/设置/调用成员,或创建比反射更快的 getter/setter(lambda 编译成委托)。例如:

                    var success = Reflector.Get(DateTime.Now, null, "Date.Year", out int value);
                    

                    或者创建一次 getter 并缓存以供重用(这样性能更高,但如果中间成员为 null,可能会抛出 NullReferenceException):

                    var getter = Reflector.CreateGetter<DateTime, int>("Date.Year", typeof(DateTime));
                    getter(DateTime.Now);
                    

                    或者,如果您想创建不同 getter 的 List&lt;Action&lt;object, object&gt;&gt;,只需为编译的委托指定基本类型(类型转换将添加到编译的 lambda 中):

                    var getter = Reflector.CreateGetter<object, object>("Date.Year", typeof(DateTime));
                    getter(DateTime.Now);
                    

                    【讨论】:

                    • 永远不要使用第 3 方库,如果您可以在合理的时间内用自己的代码在 5-10 行内实现它。
                    【解决方案19】:

                    更短的路......

                    var a = new Test { Id = 1 , Name = "A" , date = DateTime.Now};
                    var b = new Test { Id = 1 , Name = "AXXX", date = DateTime.Now };
                    
                    var compare = string.Join("",a.GetType().GetProperties().Select(x => x.GetValue(a)).ToArray())==
                                  string.Join("",b.GetType().GetProperties().Select(x => x.GetValue(b)).ToArray());
                    

                    【讨论】:

                      【解决方案20】:

                      jheddingsAlexD 都写了关于如何解析属性字符串的优秀答案。我想把我的也加入其中,因为我专门为此目的编写了一个库。

                      Pather.CSharp 的主类是Resolver。默认情况下,它可以解析属性、数组和字典条目。

                      因此,例如,如果您有这样的对象

                      var o = new { Property1 = new { Property2 = "value" } };
                      

                      想得到Property2,可以这样:

                      IResolver resolver = new Resolver();
                      var path = "Property1.Property2";
                      object result = r.Resolve(o, path); 
                      //=> "value"
                      

                      这是它可以解析的最基本的路径示例。如果你想看看它还能做什么,或者你可以如何扩展它,只需前往它的Github page

                      【讨论】:

                        【解决方案21】:

                        这是我根据其他答案得到的。对错误处理进行如此具体的处理有点矫枉过正。

                        public static T GetPropertyValue<T>(object sourceInstance, string targetPropertyName, bool throwExceptionIfNotExists = false)
                        {
                            string errorMsg = null;
                        
                            try
                            {
                                if (sourceInstance == null || string.IsNullOrWhiteSpace(targetPropertyName))
                                {
                                    errorMsg = $"Source object is null or property name is null or whitespace. '{targetPropertyName}'";
                                    Log.Warn(errorMsg);
                        
                                    if (throwExceptionIfNotExists)
                                        throw new ArgumentException(errorMsg);
                                    else
                                        return default(T);
                                }
                        
                                Type returnType = typeof(T);
                                Type sourceType = sourceInstance.GetType();
                        
                                PropertyInfo propertyInfo = sourceType.GetProperty(targetPropertyName, returnType);
                                if (propertyInfo == null)
                                {
                                    errorMsg = $"Property name '{targetPropertyName}' of type '{returnType}' not found for source object of type '{sourceType}'";
                                    Log.Warn(errorMsg);
                        
                                    if (throwExceptionIfNotExists)
                                        throw new ArgumentException(errorMsg);
                                    else
                                        return default(T);
                                }
                        
                                return (T)propertyInfo.GetValue(sourceInstance, null);
                            }
                            catch(Exception ex)
                            {
                                errorMsg = $"Problem getting property name '{targetPropertyName}' from source instance.";
                                Log.Error(errorMsg, ex);
                        
                                if (throwExceptionIfNotExists)
                                    throw;
                            }
                        
                            return default(T);
                        }
                        

                        【讨论】:

                          【解决方案22】:

                          这是我的解决方案。它也适用于 COM 对象,并允许从 COM 对象访问集合/数组项。

                          public static object GetPropValue(this object obj, string name)
                          {
                              foreach (string part in name.Split('.'))
                              {
                                  if (obj == null) { return null; }
                          
                                  Type type = obj.GetType();
                                  if (type.Name == "__ComObject")
                                  {
                                      if (part.Contains('['))
                                      {
                                          string partWithoundIndex = part;
                                          int index = ParseIndexFromPropertyName(ref partWithoundIndex);
                                          obj = Versioned.CallByName(obj, partWithoundIndex, CallType.Get, index);
                                      }
                                      else
                                      {
                                          obj = Versioned.CallByName(obj, part, CallType.Get);
                                      }
                                  }
                                  else
                                  {
                                      PropertyInfo info = type.GetProperty(part);
                                      if (info == null) { return null; }
                                      obj = info.GetValue(obj, null);
                                  }
                              }
                              return obj;
                          }
                          
                          private static int ParseIndexFromPropertyName(ref string name)
                          {
                              int index = -1;
                              int s = name.IndexOf('[') + 1;
                              int e = name.IndexOf(']');
                              if (e < s)
                              {
                                  throw new ArgumentException();
                              }
                              string tmp = name.Substring(s, e - s);
                              index = Convert.ToInt32(tmp);
                              name = name.Substring(0, s - 1);
                              return index;
                          }
                          

                          【讨论】:

                            【解决方案23】:

                            虽然最初的问题是关于如何仅使用单个字符串作为参数来获取属性的值,但在这里使用表达式而不是简单的字符串是很有意义的确保调用者从不使用硬编码的属性名称。这是一个带有用法的单行版本:

                            public static class Utils
                            ...
                                public static TVal GetPropertyValue<T, TVal>(T t, Expression<Func<T, TVal>> x)
                                    => (TVal)((x.Body as MemberExpression)?.Member as PropertyInfo)!.GetValue(t);
                            
                            ...
                                var val = Utils.GetPropertyValue(foo,  p => p.Bar);
                            

                            在可读性和错误处理方面,这是一个稍微好一点的版本:

                            public static TVal GetPropertyValue<T, TVal>(T t, Expression<Func<T, TVal>> x)
                            {
                                var m = (x.Body as MemberExpression)?.Member
                                var p = m as PropertyInfo;
                            
                                if (null == p)
                                    throw new ArgumentException($"Unknown property: {typeof(T).Name}.{(m?.Name??"???")}");
                            
                                return (TVal)p.GetValue(t);
                            }
                            

                            简而言之,您传入一个读取属性的 lambda 表达式。 lambda 的主体(粗箭头右侧的部分)是一个成员表达式,您可以从中获取成员名称,并且可以将其转换为 PropertyInfo,前提是该成员实际上是一个 Property 而不是,例如,一种方法。

                            在简短版本中,null 宽容运算符 - !在表达式中 - 告诉编译器 PropertyInfo 不会为空。这是一个很大的谎言,您将在运行时收到 NullReferenceException。如果它设法获得它,较长的版本会为您提供属性的名称。

                            PS:感谢 Oleg G. 提供此代码的初始版本 :)

                            【讨论】:

                              猜你喜欢
                              • 2016-06-16
                              • 1970-01-01
                              • 2010-11-02
                              • 2012-01-27
                              • 2020-09-29
                              • 1970-01-01
                              • 2010-11-08
                              • 2018-11-28
                              相关资源
                              最近更新 更多