Qual è il caso d'angolo più strano che hai visto in C#o in.NET?




(20)

Raccolgo alcuni casi d'angolo e rompicapo e mi piacerebbe sempre sentire di più. La pagina copre solo bit e bob in linguaggio C #, ma trovo anche interessanti le cose di base su .NET. Per esempio, eccone uno che non è sulla pagina, ma che trovo incredibile:

string x = new string(new char[0]);
string y = new string(new char[0]);
Console.WriteLine(object.ReferenceEquals(x, y));

Mi aspetto che stampi False - dopotutto, "nuovo" (con un tipo di riferimento) crea sempre un nuovo oggetto, vero? Le specifiche per C # e CLI indicano che dovrebbe. Bene, non in questo caso particolare. Stampa True, e ha eseguito su ogni versione del framework con cui l'ho testato. (Non l'ho provato su Mono, devo ammetterlo ...)

Per essere chiari, questo è solo un esempio del tipo di cose che sto cercando - non stavo particolarmente cercando discussioni / spiegazioni di questa stranezza. (Non è lo stesso del normale internamento delle stringhe, in particolare, lo interning delle stringhe normalmente non si verifica quando viene chiamato un costruttore.) Stavo davvero chiedendo un comportamento strano simile.

Qualche altra gemma in agguato là fuori?


Assegna questo!

Questo è quello che mi piace chiedere alle parti (che è probabilmente il motivo per cui non vengo più invitato):

Puoi compilare il seguente pezzo di codice?

    public void Foo()
    {
        this = new Teaser();
    }

Un semplice trucco potrebbe essere:

string cheat = @"
    public void Foo()
    {
        this = new Teaser();
    }
";

Ma la vera soluzione è questa:

public struct Teaser
{
    public void Foo()
    {
        this = new Teaser();
    }
}

Quindi è un po 'noto che i tipi di valore (structs) possono riassegnare this variabile.


C # Accessibility Puzzler

La seguente classe derivata sta accedendo a un campo privato dalla sua classe base e il compilatore guarda silenziosamente dall'altra parte:

public class Derived : Base
{
    public int BrokenAccess()
    {
        return base.m_basePrivateField;
    }
}

Il campo è davvero privato:

private int m_basePrivateField = 0;

Ti va di indovinare come possiamo compilare questo codice?

.

.

.

.

.

.

.

Risposta

Il trucco è dichiarare Derivedcome una classe interiore di Base:

public class Base
{
    private int m_basePrivateField = 0;

    public class Derived : Base
    {
        public int BrokenAccess()
        {
            return base.m_basePrivateField;
        }
    }
}

Le classi interne hanno pieno accesso ai membri della classe esterna. In questo caso, anche la classe interiore deriva dalla classe esterna. Questo ci permette di "rompere" l'incapsulamento dei membri privati.


Arrotondamento dei banchieri.

Questo non è tanto un bug o un malfunzionamento del compilatore, ma certamente uno strano caso d'angolo ...

Il .Net Framework utilizza uno schema o un arrotondamento noto come Arrotondamento di Banker.

Nell'arrotondamento dei banchieri gli 0,5 numeri sono arrotondati al numero pari più vicino, quindi

Math.Round(-0.5) == 0
Math.Round(0.5) == 0
Math.Round(1.5) == 2
Math.Round(2.5) == 2
etc...

Ciò può comportare alcuni bug inaspettati nei calcoli finanziari basati sull'arrotondamento Round-Half-Up più noto.

Questo vale anche per Visual Basic.


C # supporta le conversioni tra array e liste purché le matrici non siano multidimensionali e vi sia una relazione di ereditarietà tra i tipi ei tipi sono tipi di riferimento

object[] oArray = new string[] { "one", "two", "three" };
string[] sArray = (string[])oArray;

// Also works for IList (and IEnumerable, ICollection)
IList<string> sList = (IList<string>)oArray;
IList<object> oList = new string[] { "one", "two", "three" };

Nota che questo non funziona:

object[] oArray2 = new int[] { 1, 2, 3 }; // Error: Cannot implicitly convert type 'int[]' to 'object[]'
int[] iArray = (int[])oArray2;            // Error: Cannot convert type 'object[]' to 'int[]'

Cosa succede se si dispone di una classe generica con metodi che potrebbero essere resi ambigui a seconda degli argomenti del tipo? Mi sono imbattuto in questa situazione recentemente scrivendo un dizionario bidirezionale. Volevo scrivere metodi Get() simmetrici che restituirebbero il contrario di qualunque argomento fosse passato. Qualcosa come questo:

class TwoWayRelationship<T1, T2>
{
    public T2 Get(T1 key) { /* ... */ }
    public T1 Get(T2 key) { /* ... */ }
}

Tutto va bene se si crea un'istanza in cui T1 e T2 sono tipi diversi:

var r1 = new TwoWayRelationship<int, string>();
r1.Get(1);
r1.Get("a");

Ma se T1 e T2 sono uguali (e probabilmente se uno era una sottoclasse di un altro), è un errore del compilatore:

var r2 = new TwoWayRelationship<int, int>();
r2.Get(1);  // "The call is ambiguous..."

È interessante notare che tutti gli altri metodi nel secondo caso sono ancora utilizzabili; è solo le chiamate al metodo ora ambiguo che causa un errore del compilatore. Caso interessante, se un po 'improbabile e oscuro.


Dovrebbero aver creato 0 un intero anche quando c'è un sovraccarico della funzione enum.

Conoscevo le motivazioni del team centrale di C # per mappare 0 a enum, ma ancora, non è ortogonale come dovrebbe essere. Esempio da Npgsql .

Esempio di prova:

namespace Craft
{
    enum Symbol { Alpha = 1, Beta = 2, Gamma = 3, Delta = 4 };


   class Mate
    {
        static void Main(string[] args)
        {

            JustTest(Symbol.Alpha); // enum
            JustTest(0); // why enum
            JustTest((int)0); // why still enum

            int i = 0;

            JustTest(Convert.ToInt32(0)); // have to use Convert.ToInt32 to convince the compiler to make the call site use the object version

            JustTest(i); // it's ok from down here and below
            JustTest(1);
            JustTest("string");
            JustTest(Guid.NewGuid());
            JustTest(new DataTable());

            Console.ReadLine();
        }

        static void JustTest(Symbol a)
        {
            Console.WriteLine("Enum");
        }

        static void JustTest(object o)
        {
            Console.WriteLine("Object");
        }
    }
}

Eccone uno che ho scoperto solo di recente ...

interface IFoo
{
   string Message {get;}
}
...
IFoo obj = new IFoo("abc");
Console.WriteLine(obj.Message);

Quanto sopra sembra assurdo a prima vista, ma in realtà è legale. No, davvero (anche se mi sono perso una parte fondamentale, ma non è nulla di hacky come "aggiungi una classe chiamata IFoo " o "aggiungi un alias using come punto" IFoo in una classe ").

Vedi se riesci a capire perché, allora: chi dice che non puoi creare un'istanza di un'interfaccia?


Ho trovato un secondo caso d'angolo davvero strano che batte il mio primo da un colpo lungo.

Il metodo String.Equals (String, String, StringComparison) non è attualmente privo di effetti collaterali.

Stavo lavorando su un blocco di codice che aveva questo su una riga da solo nella parte superiore di alcune funzioni:

stringvariable1.Equals(stringvariable2, StringComparison.InvariantCultureIgnoreCase);

La rimozione di tale linea ha portato a un overflow dello stack da qualche altra parte nel programma.

Il codice risultò essere l'installazione di un gestore per quello che era essenzialmente un evento BeforeAssemblyLoad e che cercava di fare

if (assemblyfilename.EndsWith("someparticular.dll", StringComparison.InvariantCultureIgnoreCase))
{
    assemblyfilename = "someparticular_modified.dll";
}

Ormai non avrei dovuto dirtelo. L'utilizzo di una cultura che non è stata utilizzata in precedenza in un confronto di stringhe provoca un carico di assemblaggio. InvariantCulture non è un'eccezione a questo.


Penso di averti mostrato questo, ma mi piace il divertimento qui - questo ha richiesto un po 'di debug per rintracciare! (il codice originale era ovviamente più complesso e sottile ...)

    static void Foo<T>() where T : new()
    {
        T t = new T();
        Console.WriteLine(t.ToString()); // works fine
        Console.WriteLine(t.GetHashCode()); // works fine
        Console.WriteLine(t.Equals(t)); // works fine

        // so it looks like an object and smells like an object...

        // but this throws a NullReferenceException...
        Console.WriteLine(t.GetType());
    }

Allora, qual è stato T ...

Risposta: qualsiasi Nullable<T> - come int? . Tutti i metodi sono sovrascritti, tranne GetType () che non può essere; quindi è cast (in box) per oggetto (e quindi per null) per chiamare object.GetType () ... che chiama null ;-p

Aggiornamento: la trama si infittisce ... Ayende Rahien ha lanciato una simile sfida sul suo blog , ma con una where T : class, new() :

private static void Main() {
    CanThisHappen<MyFunnyType>();
}

public static void CanThisHappen<T>() where T : class, new() {
    var instance = new T(); // new() on a ref-type; should be non-null, then
    Debug.Assert(instance != null, "How did we break the CLR?");
}

Ma può essere sconfitto! Usando lo stesso riferimento indiretto usato da cose come il remoting; avvertimento: il seguente è puro male :

class MyFunnyProxyAttribute : ProxyAttribute {
    public override MarshalByRefObject CreateInstance(Type serverType) {
        return null;
    }
}
[MyFunnyProxy]
class MyFunnyType : ContextBoundObject { }

Con questo, la chiamata new() viene reindirizzata al proxy ( MyFunnyProxyAttribute ), che restituisce null . Ora vai e lavati gli occhi!


Pochi anni fa, quando lavoravamo sul programma di fedeltà, abbiamo riscontrato un problema con la quantità di punti assegnati ai clienti. Il problema era relativo al casting / conversione da double a int.

Nel codice qui sotto:

double d = 13.6;

int i1 = Convert.ToInt32(d);
int i2 = (int)d;

fa i1 == i2 ?

Si scopre che i1! = I2. A causa delle diverse politiche di arrotondamento in Converti e nell'operatore di cast i valori effettivi sono:

i1 == 14
i2 == 13

È sempre meglio chiamare Math.Ceiling () o Math.Floor () (o Math.Round con MidpointRounding che soddisfa i nostri requisiti)

int i1 = Convert.ToInt32( Math.Ceiling(d) );
int i2 = (int) Math.Ceiling(d);

Questo è il più strano che ho incontrato per caso:

public class DummyObject
{
    public override string ToString()
    {
        return null;
    }
}

Usato come segue:

DummyObject obj = new DummyObject();
Console.WriteLine("The text: " + obj.GetType() + " is " + obj);

NullReferenceException una NullReferenceException . Risulta che le aggiunte multiple sono compilate dal compilatore C # a una chiamata a String.Concat(object[]) . Prima di .NET 4, c'è un bug in questo sovraccarico di Concat in cui l'oggetto viene controllato come null, ma non come risultato di ToString ():

object obj2 = args[i];
string text = (obj2 != null) ? obj2.ToString() : string.Empty;
// if obj2 is non-null, but obj2.ToString() returns null, then text==null
int length = text.Length;

Questo è un bug di ECMA-334 §14.7.4:

L'operatore binario + esegue la concatenazione di stringhe quando uno o entrambi gli operandi sono di tipo string . Se un operando di concatenazione di stringhe è null , viene sostituita una stringa vuota. Altrimenti, qualsiasi operando non stringa viene convertito nella sua rappresentazione di stringa richiamando il metodo ToString virtuale ereditato dall'oggetto type. Se ToString restituisce null , viene sostituita una stringa vuota.


Questo è uno dei più insoliti che ho visto finora (a parte quelli qui ovviamente!):

public class Turtle<T> where T : Turtle<T>
{
}

Ti permette di dichiararlo ma non ha alcun reale utilizzo, dal momento che ti chiederà sempre di avvolgere qualsiasi classe che fai nel centro con un'altra Tartaruga.

[scherzo] Immagino che siano le tartarughe fino in fondo ... [/ scherzo]


Quando è un booleano né vero né falso?

Bill ha scoperto che puoi hackerare un booleano in modo che se A è vero e B è vero, (A e B) è falso.

Booleans hackerati


C'è qualcosa di veramente eccitante in C #, nel modo in cui gestisce le chiusure.

Invece di copiare i valori delle variabili dello stack sulla variabile free closure, fa il preprocessore magico che avvolge tutte le occorrenze della variabile in un oggetto e quindi lo sposta fuori dallo stack - direttamente nell'heap! :)

Immagino che questo faccia sì che il linguaggio C # sia ancora più funzionalmente completo (o lambda-completo huh)) rispetto a ML stesso (che usa il valore dello stack copiando AFAIK). Anche F # ha questa caratteristica, come fa C #.

Questo mi fa molto piacere, grazie ragazzi MS!

Non è un caso strano o d'angolo, però ... ma qualcosa di veramente inaspettato da un linguaggio VM stack-based :)


Da una domanda che ho chiesto non molto tempo fa:

L'operatore condizionale non può esprimere implicitamente?

Dato:

Bool aBoolValue;

Dove aBoolValueè assegnato True o False;

Quanto segue non verrà compilato:

Byte aByteValue = aBoolValue ? 1 : 0;

Ma questo:

Int anIntValue = aBoolValue ? 1 : 0;

La risposta fornita è abbastanza buona.


Ho appena trovato una piccola cosa bella oggi:

public class Base
{
   public virtual void Initialize(dynamic stuff) { 
   //...
   }
}
public class Derived:Base
{
   public override void Initialize(dynamic stuff) {
   base.Initialize(stuff);
   //...
   }
}

Questo genera errore di compilazione.

La chiamata al metodo 'Initialize' deve essere inviata dinamicamente, ma non può essere perché fa parte di un'espressione di accesso di base. Valuta la possibilità di trasmettere gli argomenti dinamici o eliminare l'accesso di base.

Se scrivo base. Inizializza (roba come oggetto); funziona perfettamente, tuttavia questa sembra essere una "parola magica" qui, poiché fa esattamente la stessa cosa, tutto è ancora ricevuto come dinamico ...


Hai mai pensato che il compilatore C # potesse generare CIL non valido? Esegui questo e otterrai un TypeLoadException:

interface I<T> {
  T M(T p);
}
abstract class A<T> : I<T> {
  public abstract T M(T p);
}
abstract class B<T> : A<T>, I<int> {
  public override T M(T p) { return p; }
  public int M(int p) { return p * 2; }
}
class C : B<int> { }

class Program {
  static void Main(string[] args) {
    Console.WriteLine(new C().M(42));
  }
}

Tuttavia non so come valga il compilatore C # 4.0.

EDIT : questo è l'output dal mio sistema:

C:\Temp>type Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1 {

  interface I<T> {
    T M(T p);
  }
  abstract class A<T> : I<T> {
    public abstract T M(T p);
  }
  abstract class B<T> : A<T>, I<int> {
    public override T M(T p) { return p; }
    public int M(int p) { return p * 2; }
  }
  class C : B<int> { }

  class Program {
    static void Main(string[] args) {
      Console.WriteLine(new C().M(11));
    }
  }

}
C:\Temp>csc Program.cs
Microsoft (R) Visual C# 2008 Compiler version 3.5.30729.1
for Microsoft (R) .NET Framework version 3.5
Copyright (C) Microsoft Corporation. All rights reserved.


C:\Temp>Program

Unhandled Exception: System.TypeLoadException: Could not load type 'ConsoleAppli
cation1.C' from assembly 'Program, Version=0.0.0.0, Culture=neutral, PublicKeyTo
ken=null'.
   at ConsoleApplication1.Program.Main(String[] args)

C:\Temp>peverify Program.exe

Microsoft (R) .NET Framework PE Verifier.  Version  3.5.30729.1
Copyright (c) Microsoft Corporation.  All rights reserved.

[token  0x02000005] Type load failed.
[IL]: Error: [C:\Temp\Program.exe : ConsoleApplication1.Program::Main][offset 0x
00000001] Unable to resolve token.
2 Error(s) Verifying Program.exe

C:\Temp>ver

Microsoft Windows XP [Version 5.1.2600]

In un'API che stiamo utilizzando, i metodi che restituiscono un oggetto dominio potrebbero restituire uno speciale "oggetto nullo". Nell'implementazione di questo, l'operatore di confronto e il Equals()metodo vengono sovrascritti per tornare truese viene confrontato con null.

Quindi un utente di questa API potrebbe avere un codice come questo:

return test != null ? test : GetDefault();

o forse un po 'più prolisso, come questo:

if (test == null)
    return GetDefault();
return test;

dove GetDefault()è un metodo che restituisce alcuni valori di default che vogliamo usare al posto di null. La sorpresa mi ha colpito quando stavo usando ReSharper e seguendo il suo consiglio di riscrivere una delle seguenti cose:

return test ?? GetDefault();

Se l'oggetto test è un oggetto nullo restituito dall'API invece che da un opportuno null, il comportamento del codice è ora cambiato, in quanto l'operatore null coalescente in realtà controlla null, non esegue operator=o Equals().


Quanto segue potrebbe essere una conoscenza generale di cui semplicemente mi mancava, ma eh. Qualche tempo fa, abbiamo avuto un bug che includeva proprietà virtuali. Estrarre un po 'il contesto, prendere in considerazione il seguente codice e applicare il breakpoint all'area specificata:

class Program
{
    static void Main(string[] args)
    {
        Derived d = new Derived();
        d.Property = "AWESOME";
    }
}

class Base
{
    string _baseProp;
    public virtual string Property 
    { 
        get 
        {
            return "BASE_" + _baseProp;
        }
        set
        {
            _baseProp = value;
            //do work with the base property which might 
            //not be exposed to derived types
            //here
            Console.Out.WriteLine("_baseProp is BASE_" + value.ToString());
        }
    }
}

class Derived : Base
{
    string _prop;
    public override string Property 
    {
        get { return _prop; }
        set 
        { 
            _prop = value; 
            base.Property = value;
        } //<- put a breakpoint here then mouse over BaseProperty, 
          //   and then mouse over the base.Property call inside it.
    }

    public string BaseProperty { get { return base.Property; } private set { } }
}

Mentre ci si trova nel Derivedcontesto dell'oggetto, è possibile ottenere lo stesso comportamento quando si aggiunge base.Propertycome orologio o si digita base.Propertynel quickwatch.

Ci ho messo un po 'di tempo per capire cosa stava succedendo. Alla fine sono stato illuminato da Quickwatch. Quando si entra in Quickwatch e si esplora l' Derivedoggetto d (o dal contesto dell'oggetto this) e si seleziona il campo base, il campo di modifica in cima a Quickwatch visualizza il seguente cast:

((TestProject1.Base)(d))

Il che significa che se la base viene sostituita come tale, la chiamata sarebbe

public string BaseProperty { get { return ((TestProject1.Base)(d)).Property; } private set { } }

per gli orologi, Quickwatch e il debug del mouse sulle descrizioni dei comandi, e avrebbe quindi senso visualizzarlo "AWESOME"invece di "BASE_AWESOME"considerare il polimorfismo. Non sono ancora sicuro del perché lo trasformerebbe in un cast, un'ipotesi è che callpotrebbe non essere disponibile dal contesto di quei moduli, e solo callvirt.

In ogni caso, che ovviamente non altera nulla in termini di funzionalità, Derived.BasePropertytornerà comunque davvero "BASE_AWESOME", e quindi questa non era la radice del nostro bug al lavoro, semplicemente un componente confuso. Ho comunque trovato interessante il modo in cui potrebbe ingannare gli sviluppatori che non sarebbero a conoscenza di questo fatto durante le loro sessioni di debug, specialmente se Basenon è esposto nel tuo progetto ma piuttosto referenziato come una DLL di terze parti, risultando in Devs solo dicendo:

"Oi, aspetta ... cosa? Omg quella DLL è come, .. fare qualcosa di divertente"


Questo è abbastanza difficile da superare. Mi sono imbattuto mentre stavo cercando di costruire un'implementazione RealProxy che supporta veramente Begin / EndInvoke (grazie MS per aver reso impossibile fare a meno di orribili hack). Questo esempio è fondamentalmente un bug nel CLR, il percorso del codice non gestito per BeginInvoke non convalida il fatto che il messaggio di ritorno da RealProxy.PrivateInvoke (e dalla mia sovrascrittura di Invoke) restituisca un'istanza di un IAsyncResult. Una volta restituito, il CLR diventa incredibilmente confuso e perde ogni idea di cosa sta succedendo, come dimostrato dai test in basso.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Remoting.Proxies;
using System.Reflection;
using System.Runtime.Remoting.Messaging;

namespace BrokenProxy
{
    class NotAnIAsyncResult
    {
        public string SomeProperty { get; set; }
    }

    class BrokenProxy : RealProxy
    {
        private void HackFlags()
        {
            var flagsField = typeof(RealProxy).GetField("_flags", BindingFlags.NonPublic | BindingFlags.Instance);
            int val = (int)flagsField.GetValue(this);
            val |= 1; // 1 = RemotingProxy, check out System.Runtime.Remoting.Proxies.RealProxyFlags
            flagsField.SetValue(this, val);
        }

        public BrokenProxy(Type t)
            : base(t)
        {
            HackFlags();
        }

        public override IMessage Invoke(IMessage msg)
        {
            var naiar = new NotAnIAsyncResult();
            naiar.SomeProperty = "o noes";
            return new ReturnMessage(naiar, null, 0, null, (IMethodCallMessage)msg);
        }
    }

    interface IRandomInterface
    {
        int DoSomething();
    }

    class Program
    {
        static void Main(string[] args)
        {
            BrokenProxy bp = new BrokenProxy(typeof(IRandomInterface));
            var instance = (IRandomInterface)bp.GetTransparentProxy();
            Func<int> doSomethingDelegate = instance.DoSomething;
            IAsyncResult notAnIAsyncResult = doSomethingDelegate.BeginInvoke(null, null);

            var interfaces = notAnIAsyncResult.GetType().GetInterfaces();
            Console.WriteLine(!interfaces.Any() ? "No interfaces on notAnIAsyncResult" : "Interfaces");
            Console.WriteLine(notAnIAsyncResult is IAsyncResult); // Should be false, is it?!
            Console.WriteLine(((NotAnIAsyncResult)notAnIAsyncResult).SomeProperty);
            Console.WriteLine(((IAsyncResult)notAnIAsyncResult).IsCompleted); // No way this works.
        }
    }
}

Produzione:

No interfaces on notAnIAsyncResult
True
o noes

Unhandled Exception: System.EntryPointNotFoundException: Entry point was not found.
   at System.IAsyncResult.get_IsCompleted()
   at BrokenProxy.Program.Main(String[] args) 




.net