用户定义的基类转换运算符


67

介绍

我知道“不允许在用户定义的基础类之间进行转换”。作为对此规则的解释,MSDN给出了“您不需要此运算符”。

我确实知道不需要用户定义的基类的转换,因为这显然是隐式完成的。但是,我确实需要基类进行转换。

在我当前的设计中,使用非托管代码的包装程序,我使用存储在Entity类中的指针。所有使用指针的类都从该Entity类派生,例如,Body类。

因此,我有:

方法一

class Entity
{
    IntPtr Pointer;

    Entity(IntPtr pointer)
    {
        this.Pointer = pointer;
    }
}

class Body : Entity
{
    Body(IntPtr pointer) : base(pointer) { }

    explicit operator Body(Entity e)
    {
        return new Body(e.Pointer);
    }
}

此演员表是非法的。(请注意,我没有费心编写访问器)。没有它,编译器允许我执行以下操作:

方法B

(Body)myEntity
...

但是,在运行时,我会得到一个例外,说明此强制转换是不可能的。

结论

因此,在这里,我需要基类进行用户定义的转换,而C#拒绝了它。使用方法A,编译器会抱怨,但是代码将在运行时逻辑上起作用。使用方法B,编译器不会抱怨,但是代码显然会在运行时失败。

在这种情况下,我感到奇怪的是,MSDN告诉我不需要此运算符,并且编译器的行为好像是隐式的(方法B)。我应该做些什么?

我知道我可以使用:

解决方案A

class Body : Entity
{
    Body(IntPtr pointer) : base(pointer) { }

    static Body FromEntity(Entity e)
    {
        return new Body(e.Pointer);
    }
}

解决方案B

class Body : Entity
{
    Body(IntPtr pointer) : base(pointer) { }

    Body(Entity e) : base(e.Pointer) { }
}

解决方案C

class Entity
{
    IntPtr Pointer;

    Entity(IntPtr pointer)
    {
        this.Pointer = pointer;
    }

    Body ToBody()
    {
        return new Body(this.Pointer);
    }
}

但老实说,所有这些语法都是可怕的,实际上应该强制转换。那么,有什么方法可以使演员阵容发挥作用?是C#设计缺陷还是我错过了可能性?好像C#对我的信任不足,无法使用他们的演员表系统编写我自己的基数转换。


6
还有人认为演员没有太多的事情吗?上流,下流,装箱/拆箱,用户定义的转换... C ++在几年前拆分了他们的演员。也许是时候C#效仿了?
Stephen Cleary 2010年

Answers:


44

这不是设计缺陷。原因如下:

Entity entity = new Body();
Body body = (Body) entity;

如果允许您在此处编写自己的用户定义的转换,那么将有两种有效的转换:尝试进行常规转换(这是引用转换,保留身份)和用户定义的转换。

应该使用哪个?您真正想要的是这样它们可以做不同的事情吗?

// Reference conversion: preserves identity
Object entity = new Body();
Body body = (Body) entity;

// User-defined conversion: creates new instance
Entity entity = new Body();
Body body = (Body) entity;

k!IMO就是那样疯狂。不要忘记,编译器决定该在编译时,仅基于编译时类型所涉及的表达式。

就我个人而言,我会使用解决方案C-甚至可能使其成为虚拟方法。这样Body 可能就超越只是回报this,如果你希望它是身份保留在可能的,但需要创建一个新的对象在哪里。


1
您不理解我的意图-我不想将“主体”转换为“实体”,这太可怕了。我想从实体到身体。问题是我不知道它是哪个类,因为我只包装了一个非托管指针。
拉兹洛

@Lazlo:对不起,我确实了解您的意图-我只是弄乱了代码示例。(变量类型是正确的,只是类型转换是错误的。我现在已经解决了这些问题。)但是,我帖子的原因仍然相同。您需要自定义转换,其中已经有一个明确的内置转换。表达这种愿望的最清晰方法是一种方法。
乔恩·斯基特

(构造函数调用也是可以接受的,但在某些情况下可能会导致代码不那么流利。)
Jon Skeet 2010年

@Jon:我对现有的转换是200%-仅在运行时会崩溃。随时尝试一下。
拉兹洛

@Lazlo:您的意思是带有InvalidCastException吗?是的,当然可以。这就是要点-为什么将新的,非常不同的转换伪装成完全不同的转换呢?
乔恩·斯基特

20

好吧,当您投射Entity到时Body,您并不是真正地将彼此投射,而是将其投射IntPtr到新实体。

为什么不从中创建显式转换运算符IntPtr

public class Entity {
    public IntPtr Pointer;

    public Entity(IntPtr pointer) {
        this.Pointer = pointer;
    }
}

public class Body : Entity {
    Body(IntPtr pointer) : base(pointer) { }

    public static explicit operator Body(IntPtr ptr) {
        return new Body(ptr);
    }

    public static void Test() {
        Entity e = new Entity(new IntPtr());
        Body body = (Body)e.Pointer;
    }
}

迄今为止最好的选择,如果赏金没有产生任何其他有价值的结果,将选择它。
拉兹洛

9

您应该使用解决方案B(构造函数参数);首先,这就是为什么使用其他建议的解决方案的原因:

  • 解决方案A只是解决方案B的包装;
  • 解决方案C只是错误的(为什么基类应该知道如何将自己转换为任何子类?)

另外,如果Body该类包含其他属性,则在执行转换时应将这些属性初始化为什么?按照OO语言的惯例,最好使用构造函数并初始化子类的属性。


+1为最后一个参数。我会考虑的。但是没有办法作为适当的演员吗?
拉兹洛

4
我对知道如何将自己转换为特定子类的基类没有任何问题……尤其是虚拟的。见证Object.ToString以及扩展方法IEnumerable.ToList,IEnumerable.ToArray。
乔恩·斯基特

2

您无法执行此操作的原因是,在一般情况下它不安全。考虑可能性。如果因为基类和派生类是可互换的而希望这样做,那么您实际上只有一个类,应该合并这两个类。如果您希望拥有转换运算符,以便能够将基数转换为派生类,则必须考虑到并非所有键入的变量都将基类指向您要转换的特定派生类的实例,因为基类类型至。这可能是这样,但你必须首先检查,或风险无效转换异常。这就是为什么人们普遍不愿垂头丧气的原因,而无非就是拖拖拉拉。我建议您重新考虑您的设计。


在我当前的包装环境中,演员阵容应该是可能的。我了解在任何其他情况下,如果您不包装定义为指针的对象,则不建议向下转换。
拉兹洛

我仍然想不出为什么如果它们确实相同,就不会仅仅合并这些类的原因。但是,如果您坚持要说的话,那么我想说最好的选择是拥有一个显式函数GetEntityFromBody()或类似函数,该函数将在给定Body对象的情况下返回Entity对象。这清楚地表明您正在做一些与众不同的事情,同时仍然允许您这样做。不应滥用演员表。
siride

@siride如果其中一个类是通用类而另一个不是通用类,则不能合并这些类。like类就是这种情况status,其中通用版本可以包含结果。基类(非泛型)应该隐式地转换为泛型版本,因为它只会存在而没有结果。这是有效状态。
道格拉斯·加斯凯尔

2

怎么样:

public class Entity {...}

public class Body : Entity
{
  public Body(Entity sourceEntity) { this.Pointer = sourceEntity.Pointer; }
}

因此,您无需编写代码:

Body someBody = new Body(previouslyUnknownEntity.Pointer);

但是你可以使用

Body someBody = new Body(previouslyUnknownEntity);

代替。

我知道,这只是表面上的变化,但很明显,您可以轻松更改内部结构。它也用在包装模式中,我不记得它的名称了(出于稍微不同的目的)。
同样清楚的是,您正在从提供的实体中创建一个新实体,因此不应像运算符/转换那样令人困惑。

注意:尚未使用编译器,因此存在输入错误的可能性。


1

(正在调用死灵协议...)

这是我的用例:

class ParseResult
{
    public static ParseResult Error(string message);
    public static ParseResult<T> Parsed<T>(T value);

    public bool IsError { get; }
    public string ErrorMessage { get; }
    public IEnumerable<string> WarningMessages { get; }

    public void AddWarning(string message);
}

class ParseResult<T> : ParseResult
{
    public static implicit operator ParseResult<T>(ParseResult result); // Fails
    public T Value { get; }
}

...

ParseResult<SomeBigLongTypeName> ParseSomeBigLongTypeName()
{
    if (SomethingIsBad)
        return ParseResult.Error("something is bad");
    return ParseResult.Parsed(new SomeBigLongTypeName());
}

这里Parsed()可以T从其参数推断出,但Error不能,但是它可以返回ParseResult可转换为的无类型ParseResult<T>-否则将返回此错误。解决方法是返回并从子类型转换:

class ParseResult
{
    public static ErrorParseResult Error(string message);
    ...
}

class ErrorParseResult : ParseResult {}

class ParseResult<T>
{
    public static implicit operator ParseResult<T>(ErrorParseResult result);
    ...
}

一切都快乐!


0

似乎引用相等不是您关心的问题,然后您可以说:

  • public class Entity {
        public sealed class To<U> where U : Entity {
            public static implicit operator To<U>(Entity entity) {
                return new To<U> { m_handle=entity.Pointer };
            }
    
            public static implicit operator U(To<U> x) {
                return (U)Activator.CreateInstance(typeof(U), x.m_handle);
            }
    
            To() { // not exposed
            }
    
            IntPtr m_handle; // not exposed
        }
    
        IntPtr Pointer; // not exposed
    
        public Entity(IntPtr pointer) {
            this.Pointer=pointer;
        }
    }
    

    public class Body:Entity {
        public Body(IntPtr pointer) : base(pointer) {
        }
    }
    
    // added for the extra demonstration
    public class Context:Body {
        public Context(IntPtr pointer) : base(pointer) {
        }
    }
    

  • 测试

    public static class TestClass {
        public static void TestMethod() {
            Entity entity = new Entity((IntPtr)0x1234);
            Body body = (Entity.To<Body>)entity;
            Context context = (Body.To<Context>)body;
        }
    }
    

您没有编写访问器,但是我考虑了封装,以不公开它们的指针。在此实现的框架下,使用了一个不在继承链中中间类,而是将转换链化了

Activator这里涉及的好处是不添加已经约束并具有参数化构造函数的额外new()约束。尽管是公开的但密封的,但没有公开其构造函数,但只能从转换运算符实例化它。UEntityTo<U>

在测试代​​码中,实体实际上转换为通用To<U>对象,然后转换为目标类型,从body到的额外演示也是如此context。因为To<U>是嵌套类,所以它可以访问Pointer包含类的私有类,因此我们可以完成事情而无需暴露指针。

好,就是这样。



0

老实说,我认为最初的要求被误解了。

考虑一个简单的情况,其中基类仅充当相关类的分组。

例如:

class Parent ...
class Child1 : Parent ...
class Child2 : Parent ...

程序员知道如何从一个子类显式转换为另一个子类的地方。

Parent类可用于,例如,在:

Dictionary<string, Parent>

我认为最初的要求是:

如何编码:

Class1 v1 = ...
Class2 v2 = ...

v1 = v2;

内部Parent有显式代码来执行从对象Class2Class1对象的转换。

我的代码中有这种确切的情况。

我设法做的最好的事情就是向Parent类添加一个属性,该属性知道如何进行转换并返回正确的类型化对象。

这迫使我编写代码:

v1 = v2.AsClass1;

凡财产AsClass1Parent知道如何从做实际转换Class2Class1

老实说,这是一个代码错误(丑陋;有损于简单性,可以使表达式荒谬而冗长,晦涩难懂,最令人讨厌的是它缺乏优雅感),但这是我能想到的最好的方法。

而且,是的,您猜对了,Parent该类还包含AsClass2方法:-)

我要做的就是:

v1 = v2;

并使编译器静默调用我指定的方法进行转换。

我真的不明白为什么编译器会支持这个:-(

在我看来,这真的没有什么不同:

int v1;
decimal v2;
. . .
v1 = (int)v2;

编译器知道静默调用某些内置转换方法。


0

虽然这是一个古老的讨论,但是我只是想补充一些自己的经验。

在类库中,我们曾经有一个数学对象,例如2D点和2D向量。由于两个类的对象的特征大部分相同(尽管并不完全相同,因此需要两个类),因此,其思想是定义aVector2DPoint2D从其派生。这样可以省去很多重复的定义,但是无法实现从向量到点的自定义转换运算符。

因此,由于我们强烈希望在代码中有意地交换类型,所以我们决定放弃派生的想法,独立声明两个类并引入隐式转换运算符。然后,我们可以自由地交换代码中的两种类型。


0

gg,我最终只是在修改后的实体中执行了一个简单的Cast()方法。也许我只是遗漏了要点,但是我需要修改基本类型,以便将代码保留在新对象中以执行x。如果编译器允许我使用公共静态显式运算符。继承将其弄乱了显式强制转换运算符。

用法:

var newItem = UpgradedEnity(dbItem);
var stuff = newItem.Get();

样品:

public class UpgradedEnity : OriginalEnity_Type
    {
        public string Get()
        {
            foreach (var item in this.RecArray)
            {
                //do something
            }
            return "return something";
        }

        public static UpgradedEnity Cast(OriginalEnity_Type v)
        {
            var rv = new UpgradedEnity();
            PropertyCopier<OriginalEnity_Type, UpgradedEnity>.Copy(v, rv);
            return rv;
        }

        public class PropertyCopier<TParent, TChild> where TParent : class
                                            where TChild : class
        {
            public static void Copy(TParent from, TChild to)
            {
                var parentProperties = from.GetType().GetProperties();
                var childProperties = to.GetType().GetProperties();

                foreach (var parentProperty in parentProperties)
                {
                    foreach (var childProperty in childProperties)
                    {
                        if (parentProperty.Name == childProperty.Name && parentProperty.PropertyType == childProperty.PropertyType)
                        {
                            childProperty.SetValue(to, parentProperty.GetValue(from));
                            break;
                        }
                    }
                }
            }
        }
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.