Why doesn't 'ref' and 'out' support polymorphism?

徘徊边缘 提交于 2019-12-17 02:09:12

问题


Take the following:

class A {}

class B : A {}

class C
{
    C()
    {
        var b = new B();
        Foo(b);
        Foo2(ref b); // <= compile-time error: 
                     // "The 'ref' argument doesn't match the parameter type"
    }

    void Foo(A a) {}

    void Foo2(ref A a) {}  
}

Why does the above compile-time error occur? This happens with both ref and out arguments.


回答1:


=============

UPDATE: I used this answer as the basis for this blog entry:

Why do ref and out parameters not allow type variation?

See the blog page for more commentary on this issue. Thanks for the great question.

=============

Let's suppose you have classes Animal, Mammal, Reptile, Giraffe, Turtle and Tiger, with the obvious subclassing relationships.

Now suppose you have a method void M(ref Mammal m). M can both read and write m.


Can you pass a variable of type Animal to M?

No. That variable could contain a Turtle, but M will assume that it contains only Mammals. A Turtle is not a Mammal.

Conclusion 1: ref parameters cannot be made "bigger". (There are more animals than mammals, so the variable is getting "bigger" because it can contain more things.)


Can you pass a variable of type Giraffe to M?

No. M can write to m, and M might want to write a Tiger into m. Now you've put a Tiger into a variable which is actually of type Giraffe.

Conclusion 2: ref parameters cannot be made "smaller".


Now consider N(out Mammal n).

Can you pass a variable of type Giraffe to N?

No. N can write to n, and N might want to write a Tiger.

Conclusion 3: out parameters cannot be made "smaller".


Can you pass a variable of type Animal to N?

Hmm.

Well, why not? N cannot read from n, it can only write to it, right? You write a Tiger to a variable of type Animal and you're all set, right?

Wrong. The rule is not "N can only write to n".

The rules are, briefly:

1) N has to write to n before N returns normally. (If N throws, all bets are off.)

2) N has to write something to n before it reads something from n.

That permits this sequence of events:

  • Declare a field x of type Animal.
  • Pass x as an out parameter to N.
  • N writes a Tiger into n, which is an alias for x.
  • On another thread, someone writes a Turtle into x.
  • N attempts to read the contents of n, and discovers a Turtle in what it thinks is a variable of type Mammal.

Clearly we want to make that illegal.

Conclusion 4: out parameters cannot be made "larger".


Final conclusion: Neither ref nor out parameters may vary their types. To do otherwise is to break verifiable type safety.

If these issues in basic type theory interest you, consider reading my series on how covariance and contravariance work in C# 4.0.




回答2:


Because in both cases you must be able to assign value to ref/out parameter.

If you try to pass b into Foo2 method as reference, and in Foo2 you try to assing a = new A(), this would be invalid.
Same reason you can't write:

B b = new A();



回答3:


You're struggling with the classic OOP problem of covariance (and contravariance), see wikipedia: much as this fact may defy intuitive expectations, it's mathematically impossible to allow substitution of derived classes in lieu of base ones for mutable (assignable) arguments (and also containers whose items are assignable, for just the same reason) while still respecting Liskov's principle. Why that is so is sketched in the existing answers, and explored more deeply in these wiki articles and links therefrom.

OOP languages that appear to do so while remaining traditionally statically typesafe are "cheating" (inserting hidden dynamic type checks, or requiring compile-time examination of ALL sources to check); the fundamental choice is: either give up on this covariance and accept practitioners' puzzlement (as C# does here), or move to a dynamic typing approach (as the very first OOP language, Smalltalk, did), or move to immutable (single-assignment) data, like functional languages do (under immutability, you can support covariance, and also avoid other related puzzles such as the fact that you cannot have Square subclass Rectangle in a mutable-data world).




回答4:


Consider:

class C : A {}
class B : A {}

void Foo2(ref A a) { a = new C(); } 

B b = null;
Foo2(ref b);

It would violate type-safety




回答5:


While the other responses have succinctly explained the reasoning behind this behavior, I think it's worth mentioning that if you really need to do something of this nature you can accomplish similar functionality by making Foo2 into a generic method, as such:

class A {}

class B : A {}

class C
{
    C()
    {
        var b = new B();
        Foo(b);
        Foo2(ref b); // <= no compile error!
    }

    void Foo(A a) {}

    void Foo2<AType> (ref AType a) where AType: A {}  
}



回答6:


Because giving Foo2 a ref B would result in a malformed object because Foo2 only knows how to fill A part of B.




回答7:


Isn't that the compiler telling you it would like you to explicitly cast the object so that it can be sure you know what your intentions are?

Foo2(ref (A)b)



回答8:


Makes sense from a safety perspective, but I would have preferred it if the compiler gave a warning instead of an error, since there are legitimate uses of polymoprhic objects passed by reference. e.g.

class Derp : interfaceX
{
   int somevalue=0; //specified that this class contains somevalue by interfaceX
   public Derp(int val)
    {
    somevalue = val;
    }

}


void Foo(ref object obj){
    int result = (interfaceX)obj.somevalue;
    //do stuff to result variable... in my case data access
    obj = Activator.CreateInstance(obj.GetType(), result);
}

main()
{
   Derp x = new Derp();
   Foo(ref Derp);
}

This won't compile, but would it work?




回答9:


If you use practical examples for your types, you'll see it:

SqlConnection connection = new SqlConnection();
Foo(ref connection);

And now you have your function that takes the ancestor (i.e. Object):

void Foo2(ref Object connection) { }

What can possibly be wrong with that?

void Foo2(ref Object connection)
{
   connection = new Bitmap();
}

You just managed to assign a Bitmap to your SqlConnection.

That's no good.


Try again with others:

SqlConnection conn = new SqlConnection();
Foo2(ref conn);

void Foo2(ref DbConnection connection)
{
    conn = new OracleConnection();
}

You stuffed an OracleConnection over-top of your SqlConnection.




回答10:


In my case my function accepted an object and I couldn't send in anything so I simply did

object bla = myVar;
Foo(ref bla);

And that works

My Foo is in VB.NET and it checks for type inside and does a lot of logic

I apologize if my answer is duplicate but others were too long



来源:https://stackoverflow.com/questions/1207144/why-doesnt-ref-and-out-support-polymorphism

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!