คลาส C # สามารถสืบทอดคุณสมบัติจากอินเทอร์เฟซได้หรือไม่?


114

สิ่งนี้ดูเหมือนจะบ่งบอกเป็นนัยว่า "ไม่" ซึ่งเป็นโชคร้าย.

[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class,
 AllowMultiple = true, Inherited = true)]
public class CustomDescriptionAttribute : Attribute
{
    public string Description { get; private set; }

    public CustomDescriptionAttribute(string description)
    {
        Description = description;
    }
}

[CustomDescription("IProjectController")]
public interface IProjectController
{
    void Create(string projectName);
}

internal class ProjectController : IProjectController
{
    public void Create(string projectName)
    {
    }
}

[TestFixture]
public class CustomDescriptionAttributeTests
{
    [Test]
    public void ProjectController_ShouldHaveCustomDescriptionAttribute()
    {
        Type type = typeof(ProjectController);
        object[] attributes = type.GetCustomAttributes(
            typeof(CustomDescriptionAttribute),
            true);

        // NUnit.Framework.AssertionException:   Expected: 1   But was:  0
        Assert.AreEqual(1, attributes.Length);
    }
}

คลาสสามารถสืบทอดแอตทริบิวต์จากอินเทอร์เฟซได้หรือไม่? หรือฉันเห่าต้นไม้ผิดที่นี่?

คำตอบ:


73

ไม่เมื่อใดก็ตามที่ใช้อินเทอร์เฟซหรือการลบล้างสมาชิกในคลาสที่ได้รับคุณจะต้องประกาศแอตทริบิวต์อีกครั้ง

หากคุณสนใจเฉพาะ ComponentModel (ไม่ใช่การสะท้อนโดยตรง) มีวิธี ( [AttributeProvider]) ในการแนะนำแอ็ตทริบิวต์จากประเภทที่มีอยู่ (เพื่อหลีกเลี่ยงการทำซ้ำ) แต่ใช้ได้เฉพาะกับคุณสมบัติและการใช้งานดัชนีเท่านั้น

ตัวอย่างเช่น:

using System;
using System.ComponentModel;
class Foo {
    [AttributeProvider(typeof(IListSource))]
    public object Bar { get; set; }

    static void Main() {
        var bar = TypeDescriptor.GetProperties(typeof(Foo))["Bar"];
        foreach (Attribute attrib in bar.Attributes) {
            Console.WriteLine(attrib);
        }
    }
}

เอาท์พุท:

System.SerializableAttribute
System.ComponentModel.AttributeProviderAttribute
System.ComponentModel.EditorAttribute
System.Runtime.InteropServices.ComVisibleAttribute
System.Runtime.InteropServices.ClassInterfaceAttribute
System.ComponentModel.TypeConverterAttribute
System.ComponentModel.MergablePropertyAttribute

คุณแน่ใจเกี่ยวกับเรื่องนี้หรือไม่? เมธอด MemberInfo.GetCustomAttributes ใช้อาร์กิวเมนต์ที่บอกว่าควรค้นหาแผนผังการสืบทอดหรือไม่
Rune Grimstad

3
อืมมม ฉันเพิ่งสังเกตว่าคำถามเกี่ยวกับการสืบทอดคุณสมบัติจากอินเทอร์เฟซที่ไม่ได้มาจากคลาสพื้นฐาน
Rune Grimstad

มีเหตุผลใดที่ต้องใส่แอตทริบิวต์บนอินเทอร์เฟซหรือไม่?
Ryan Penfold

5
@Ryan - แน่นอน: สำหรับการอธิบายอินเทอร์เฟซ ตัวอย่างเช่นสัญญาบริการ
Marc Gravell

3
Marc (และ @Rune): ใช่ OP นั้นเกี่ยวกับอินเทอร์เฟซ แต่ประโยคแรกของคำตอบของคุณอาจทำให้สับสน: "... หรือแทนที่สมาชิกในคลาสที่ได้รับ ... " - ไม่จำเป็นต้องเป็นจริงเสมอไป คุณสามารถมีคลาสของคุณสืบทอดคุณสมบัติจากคลาสพื้นฐานได้ คุณไม่สามารถทำได้ด้วยอินเทอร์เฟซเท่านั้น ดูเพิ่มเติมที่: stackoverflow.com/questions/12106566/…
chiccodoro

39

คุณสามารถกำหนดวิธีการขยายที่มีประโยชน์ ...

Type type = typeof(ProjectController);
var attributes = type.GetCustomAttributes<CustomDescriptionAttribute>( true );

นี่คือวิธีการขยาย:

/// <summary>Searches and returns attributes. The inheritance chain is not used to find the attributes.</summary>
/// <typeparam name="T">The type of attribute to search for.</typeparam>
/// <param name="type">The type which is searched for the attributes.</param>
/// <returns>Returns all attributes.</returns>
public static T[] GetCustomAttributes<T>( this Type type ) where T : Attribute
{
  return GetCustomAttributes( type, typeof( T ), false ).Select( arg => (T)arg ).ToArray();
}

/// <summary>Searches and returns attributes.</summary>
/// <typeparam name="T">The type of attribute to search for.</typeparam>
/// <param name="type">The type which is searched for the attributes.</param>
/// <param name="inherit">Specifies whether to search this member's inheritance chain to find the attributes. Interfaces will be searched, too.</param>
/// <returns>Returns all attributes.</returns>
public static T[] GetCustomAttributes<T>( this Type type, bool inherit ) where T : Attribute
{
  return GetCustomAttributes( type, typeof( T ), inherit ).Select( arg => (T)arg ).ToArray();
}

/// <summary>Private helper for searching attributes.</summary>
/// <param name="type">The type which is searched for the attribute.</param>
/// <param name="attributeType">The type of attribute to search for.</param>
/// <param name="inherit">Specifies whether to search this member's inheritance chain to find the attribute. Interfaces will be searched, too.</param>
/// <returns>An array that contains all the custom attributes, or an array with zero elements if no attributes are defined.</returns>
private static object[] GetCustomAttributes( Type type, Type attributeType, bool inherit )
{
  if( !inherit )
  {
    return type.GetCustomAttributes( attributeType, false );
  }

  var attributeCollection = new Collection<object>();
  var baseType = type;

  do
  {
    baseType.GetCustomAttributes( attributeType, true ).Apply( attributeCollection.Add );
    baseType = baseType.BaseType;
  }
  while( baseType != null );

  foreach( var interfaceType in type.GetInterfaces() )
  {
    GetCustomAttributes( interfaceType, attributeType, true ).Apply( attributeCollection.Add );
  }

  var attributeArray = new object[attributeCollection.Count];
  attributeCollection.CopyTo( attributeArray, 0 );
  return attributeArray;
}

/// <summary>Applies a function to every element of the list.</summary>
private static void Apply<T>( this IEnumerable<T> enumerable, Action<T> function )
{
  foreach( var item in enumerable )
  {
    function.Invoke( item );
  }
}

ปรับปรุง:

นี่คือเวอร์ชันที่สั้นกว่าตามที่ SimonD เสนอในความคิดเห็น:

private static IEnumerable<T> GetCustomAttributesIncludingBaseInterfaces<T>(this Type type)
{
  var attributeType = typeof(T);
  return type.GetCustomAttributes(attributeType, true).
    Union(type.GetInterfaces().
    SelectMany(interfaceType => interfaceType.GetCustomAttributes(attributeType, true))).
    Distinct().Cast<T>();
}

1
สิ่งนี้ได้รับเฉพาะแอตทริบิวต์ระดับชนิดไม่ใช่คุณสมบัติเขตข้อมูลหรือสมาชิกใช่ไหม
Maslow

22
ดีมากฉันใช้เวอร์ชันที่สั้นกว่านี้เป็นการส่วนตัวตอนนี้: IEnumerable แบบคงที่ส่วนตัว <T> GetCustomAttributesInc รวมBaseInterfaces <T> (ประเภทนี้) {var attributeType = typeof (T); return type.GetCustomAttributes (attributeType, true) .Union (type.GetInterfaces (). SelectMany (interfaceType => interfaceType.GetCustomAttributes (attributeType, true))). Distinct (). Cast <T> (); }
Simon D.

1
@SimonD.: และโซลูชัน refactored ของคุณเร็วขึ้น
mynkow

1
@SimonD นี่เป็นคำตอบที่คุ้มค่าแทนที่จะเป็นความคิดเห็น
Nick N.

มีเหตุผลใดบ้างที่จะไม่แทนที่Applyด้วยบิวท์อินForEachจากMicrosoft.Practices.ObjectBuilder2
Jacob Brewer

29

บทความโดย Brad Wilson เกี่ยวกับเรื่องนี้: Interface Attributes! = Class Attributes

สรุป: คลาสไม่ได้รับการสืบทอดจากอินเทอร์เฟซพวกเขาใช้มัน ซึ่งหมายความว่าแอตทริบิวต์ไม่ได้เป็นส่วนหนึ่งของการนำไปใช้โดยอัตโนมัติ

หากคุณต้องการสืบทอดแอตทริบิวต์ให้ใช้คลาสพื้นฐานแบบนามธรรมแทนที่จะเป็นอินเทอร์เฟซ


จะเกิดอะไรขึ้นถ้าคุณมีอินเทอร์เฟซหลายตัวที่คุณกำลังใช้งานอยู่? คุณไม่สามารถเปลี่ยนอินเทอร์เฟซเหล่านั้นเป็นคลาสนามธรรมได้เนื่องจาก C # ไม่มีอยู่ในประเภทการสืบทอดหลายรายการ
Andy

10

แม้ว่าคลาส C # จะไม่สืบทอดคุณลักษณะจากอินเทอร์เฟซ แต่ก็มีทางเลือกที่มีประโยชน์เมื่อเชื่อมโยงโมเดลใน ASP.NET MVC3

หากคุณประกาศให้โมเดลของมุมมองเป็นอินเทอร์เฟซแทนที่จะเป็นชนิดคอนกรีตมุมมองและตัวยึดแบบจำลองจะใช้แอ็ตทริบิวต์ (เช่น[Required]หรือ[DisplayName("Foo")]จากอินเทอร์เฟซเมื่อแสดงผลและตรวจสอบโมเดล:

public interface IModel {
    [Required]
    [DisplayName("Foo Bar")]
    string FooBar { get; set; }
} 

public class Model : IModel {
    public string FooBar { get; set; }
}

จากนั้นในมุมมอง:

@* Note use of interface type for the view model *@
@model IModel 

@* This control will receive the attributes from the interface *@
@Html.EditorFor(m => m.FooBar)

4

นี่เป็นข้อมูลเพิ่มเติมสำหรับผู้ที่ต้องการแยกแอตทริบิวต์จากคุณสมบัติที่อาจมีอยู่บนอินเทอร์เฟซที่ใช้งาน เนื่องจากแอตทริบิวต์เหล่านั้นไม่ได้เป็นส่วนหนึ่งของคลาสนี้จึงทำให้คุณสามารถเข้าถึงได้ โปรดทราบว่าฉันมีคลาสคอนเทนเนอร์ง่ายๆที่ให้คุณเข้าถึง PropertyInfo - นั่นคือสิ่งที่ฉันต้องการสำหรับ แฮ็คตามที่คุณต้องการ นี้ทำงานได้ดีสำหรับฉัน.

public static class CustomAttributeExtractorExtensions
{
    /// <summary>
    /// Extraction of property attributes as well as attributes on implemented interfaces.
    /// This will walk up recursive to collect any interface attribute as well as their parent interfaces.
    /// </summary>
    /// <typeparam name="TAttributeType"></typeparam>
    /// <param name="typeToReflect"></param>
    /// <returns></returns>
    public static List<PropertyAttributeContainer<TAttributeType>> GetPropertyAttributesFromType<TAttributeType>(this Type typeToReflect)
        where TAttributeType : Attribute
    {
        var list = new List<PropertyAttributeContainer<TAttributeType>>();

        // Loop over the direct property members
        var properties = typeToReflect.GetProperties();

        foreach (var propertyInfo in properties)
        {
            // Get the attributes as well as from the inherited classes (true)
            var attributes = propertyInfo.GetCustomAttributes<TAttributeType>(true).ToList();
            if (!attributes.Any()) continue;

            list.AddRange(attributes.Select(attr => new PropertyAttributeContainer<TAttributeType>(attr, propertyInfo)));
        }

        // Look at the type interface declarations and extract from that type.
        var interfaces = typeToReflect.GetInterfaces();

        foreach (var @interface in interfaces)
        {
            list.AddRange(@interface.GetPropertyAttributesFromType<TAttributeType>());
        }

        return list;

    }

    /// <summary>
    /// Simple container for the Property and Attribute used. Handy if you want refrence to the original property.
    /// </summary>
    /// <typeparam name="TAttributeType"></typeparam>
    public class PropertyAttributeContainer<TAttributeType>
    {
        internal PropertyAttributeContainer(TAttributeType attribute, PropertyInfo property)
        {
            Property = property;
            Attribute = attribute;
        }

        public PropertyInfo Property { get; private set; }

        public TAttributeType Attribute { get; private set; }
    }
}

0

แก้ไข: สิ่งนี้ครอบคลุมถึงการสืบทอดแอตทริบิวต์จากอินเทอร์เฟซบนสมาชิก (รวมถึงคุณสมบัติ) มีคำตอบง่ายๆด้านบนสำหรับคำจำกัดความประเภท ฉันเพิ่งโพสต์สิ่งนี้เพราะฉันพบว่ามันเป็นข้อ จำกัด ที่น่ารำคาญและต้องการแบ่งปันวิธีแก้ปัญหา :)

อินเทอร์เฟซคือการสืบทอดหลายรายการและทำงานเป็นการสืบทอดในระบบชนิด ไม่มีเหตุผลที่ดีสำหรับเรื่องประเภทนี้ การสะท้อนเป็นเรื่องเล็กน้อย ฉันได้เพิ่มความคิดเห็นเพื่ออธิบายเรื่องไร้สาระ

(นี่คือ. NET 3.5 เพราะมันเกิดขึ้นกับสิ่งที่โครงการที่ฉันกำลังทำอยู่ในขณะนี้)

// in later .NETs, you can cache reflection extensions using a static generic class and
// a ConcurrentDictionary. E.g.
//public static class Attributes<T> where T : Attribute
//{
//    private static readonly ConcurrentDictionary<MemberInfo, IReadOnlyCollection<T>> _cache =
//        new ConcurrentDictionary<MemberInfo, IReadOnlyCollection<T>>();
//
//    public static IReadOnlyCollection<T> Get(MemberInfo member)
//    {
//        return _cache.GetOrAdd(member, GetImpl, Enumerable.Empty<T>().ToArray());
//    }
//    //GetImpl as per code below except that recursive steps re-enter via the cache
//}

public static List<T> GetAttributes<T>(this MemberInfo member) where T : Attribute
{
    // determine whether to inherit based on the AttributeUsage
    // you could add a bool parameter if you like but I think it defeats the purpose of the usage
    var usage = typeof(T).GetCustomAttributes(typeof(AttributeUsageAttribute), true)
        .Cast<AttributeUsageAttribute>()
        .FirstOrDefault();
    var inherit = usage != null && usage.Inherited;

    return (
        inherit
            ? GetAttributesRecurse<T>(member)
            : member.GetCustomAttributes(typeof (T), false).Cast<T>()
        )
        .Distinct()  // interfaces mean duplicates are a thing
        // note: attribute equivalence needs to be overridden. The default is not great.
        .ToList();
}

private static IEnumerable<T> GetAttributesRecurse<T>(MemberInfo member) where T : Attribute
{
    // must use Attribute.GetCustomAttribute rather than MemberInfo.GetCustomAttribute as the latter
    // won't retrieve inherited attributes from base *classes*
    foreach (T attribute in Attribute.GetCustomAttributes(member, typeof (T), true))
        yield return attribute;

    // The most reliable target in the interface map is the property get method.
    // If you have set-only properties, you'll need to handle that case. I generally just ignore that
    // case because it doesn't make sense to me.
    PropertyInfo property;
    var target = (property = member as PropertyInfo) != null ? property.GetGetMethod() : member;

    foreach (var @interface in member.DeclaringType.GetInterfaces())
    {
        // The interface map is two aligned arrays; TargetMethods and InterfaceMethods.
        var map = member.DeclaringType.GetInterfaceMap(@interface);
        var memberIndex = Array.IndexOf(map.TargetMethods, target); // see target above
        if (memberIndex < 0) continue;

        // To recurse, we still need to hit the property on the parent interface.
        // Why don't we just use the get method from the start? Because GetCustomAttributes won't work.
        var interfaceMethod = property != null
            // name of property get method is get_<property name>
            // so name of parent property is substring(4) of that - this is reliable IME
            ? @interface.GetProperty(map.InterfaceMethods[memberIndex].Name.Substring(4))
            : (MemberInfo) map.InterfaceMethods[memberIndex];

        // Continuation is the word to google if you don't understand this
        foreach (var attribute in interfaceMethod.GetAttributes<T>())
            yield return attribute;
    }
}

การทดสอบ Barebones NUnit

[TestFixture]
public class GetAttributesTest
{
    [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)]
    private sealed class A : Attribute
    {
        // default equality for Attributes is apparently semantic
        public override bool Equals(object obj)
        {
            return ReferenceEquals(this, obj);
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }

    [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
    private sealed class ANotInherited : Attribute { }

    public interface Top
    {
        [A, ANotInherited]
        void M();

        [A, ANotInherited]
        int P { get; }
    }

    public interface Middle : Top { }

    private abstract class Base
    {
        [A, ANotInherited]
        public abstract void M();

        [A, ANotInherited]
        public abstract int P { get; }
    }

    private class Bottom : Base, Middle
    {
        [A, ANotInherited]
        public override void M()
        {
            throw new NotImplementedException();
        }

        [A, ANotInherited]
        public override int P { get { return 42; } }
    }

    [Test]
    public void GetsAllInheritedAttributesOnMethods()
    {
        var attributes = typeof (Bottom).GetMethod("M").GetAttributes<A>();
        attributes.Should()
            .HaveCount(3, "there are 3 inherited copies in the class heirarchy and A is inherited");
    }

    [Test]
    public void DoesntGetNonInheritedAttributesOnMethods()
    {
        var attributes = typeof (Bottom).GetMethod("M").GetAttributes<ANotInherited>();
        attributes.Should()
            .HaveCount(1, "it shouldn't get copies of the attribute from base classes for a non-inherited attribute");
    }

    [Test]
    public void GetsAllInheritedAttributesOnProperties()
    {
        var attributes = typeof(Bottom).GetProperty("P").GetAttributes<A>();
        attributes.Should()
            .HaveCount(3, "there are 3 inherited copies in the class heirarchy and A is inherited");
    }

    [Test]
    public void DoesntGetNonInheritedAttributesOnProperties()
    {
        var attributes = typeof(Bottom).GetProperty("P").GetAttributes<ANotInherited>();
        attributes.Should()
            .HaveCount(1, "it shouldn't get copies of the attribute from base classes for a non-inherited attribute");
    }
}

0

เพิ่มอินเทอร์เฟซที่มีคุณสมบัติที่มีแอตทริบิวต์ / แอตทริบิวต์ที่กำหนดเองที่แนบมากับคุณสมบัติเดียวกันกับที่คลาสมี เราสามารถแยกอินเทอร์เฟซของคลาสโดยใช้คุณสมบัติ Visual studio refactor ให้คลาสบางส่วนใช้อินเทอร์เฟซนั้น

ตอนนี้รับวัตถุ "Type" ของคลาสออบเจ็กต์และรับแอตทริบิวต์ที่กำหนดเองจากข้อมูลคุณสมบัติโดยใช้ getProperties บนวัตถุ Type สิ่งนี้จะไม่ให้แอ็ตทริบิวต์แบบกำหนดเองบนคลาสอ็อบเจ็กต์เนื่องจากคุณสมบัติคลาสไม่ได้แนบ / สืบทอดคุณสมบัติของอินเทอร์เฟซ

ตอนนี้เรียก GetInterface (NameOfImplemetedInterfaceByclass) บนวัตถุ Type ของคลาสที่ดึงมาด้านบน สิ่งนี้จะให้ออบเจ็กต์ "Type" ของอินเทอร์เฟซ เราควรรู้จัก NAME ของอินเทอร์เฟซที่ใช้งาน จาก Type object รับข้อมูลคุณสมบัติและหากคุณสมบัติของอินเทอร์เฟซมีแอตทริบิวต์ที่กำหนดเองใด ๆ แนบมาข้อมูลคุณสมบัติจะให้รายการแอตทริบิวต์ที่กำหนดเอง คลาสการนำไปใช้งานต้องจัดเตรียมการใช้งานคุณสมบัติของอินเทอร์เฟซ จับคู่ชื่อคุณสมบัติเฉพาะของคลาสออบเจ็กต์ภายในรายการข้อมูลคุณสมบัติของอินเทอร์เฟซเพื่อรับรายการแอตทริบิวต์ที่กำหนดเอง

วิธีนี้จะได้ผล


0

แม้ว่าคำตอบของฉันจะช้าและเฉพาะเจาะจงสำหรับบางกรณี แต่ฉันก็อยากจะเพิ่มความคิด ตามที่แนะนำไว้ในคำตอบอื่นการสะท้อนกลับหรือวิธีการอื่น ๆ จะทำได้

ในกรณีของฉันจำเป็นต้องใช้คุณสมบัติ (การประทับเวลา) ในทุกรุ่นเพื่อให้เป็นไปตามข้อกำหนดบางประการ (แอตทริบิวต์การตรวจสอบการทำงานพร้อมกัน) ในโครงการหลักของกรอบงานเอนทิตี เราสามารถเพิ่ม [] เหนือคุณสมบัติของคลาสทั้งหมด (เพิ่มในอินเทอร์เฟซ IModel ที่โมเดลใช้งานไม่ได้) แต่ฉันประหยัดเวลาผ่าน Fluent API ซึ่งมีประโยชน์ในกรณีเหล่านี้ ใน API ได้อย่างคล่องแคล่วฉันสามารถตรวจสอบชื่อคุณสมบัติเฉพาะในทุกรุ่นและตั้งค่าเป็น IsConcurrencyToken () ได้ใน 1 บรรทัด !!

var props = from e in modelBuilder.Model.GetEntityTypes()
            from p in e.GetProperties()
            select p;
props.Where(p => p.PropertyInfo.Name == "ModifiedTime").ToList().ForEach(p => { p.IsConcurrencyToken = true; });

ในทำนองเดียวกันหากคุณต้องการเพิ่มแอตทริบิวต์ใด ๆ ในชื่อคุณสมบัติเดียวกันในคลาส / รุ่น 100 เราสามารถใช้เมธอด api ได้อย่างคล่องแคล่วสำหรับตัวแก้ไขแอตทริบิวต์ inbuilt หรือกำหนดเอง แม้ว่า API ที่คล่องแคล่ว EF (ทั้ง core และ EF6) อาจใช้การสะท้อนเบื้องหลัง แต่เราก็ประหยัดได้ :)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.