0

In the following code, hs.Add(...) cannot compile. Declare hs as dynamic should work but I'm trying not to use it. Is there a type-safe way?

void F(string colName, DbDataReader reader1, DbDataReader reader2)
{
    // .... loop reader1.Read(); reader2.Read()
    var x = reader1[colName];
    var type = x.GetType();
    var hs = Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(type)); 
      // change var to dynamic will work. Trying not to use it.
    hs.Add(x); // Error
    // ...
    hs.Contains(reader2[colName]); // Error

I tried the following since the type of compile-time type of x is unknown.

var hs = (HashSet<object>)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(type)); 

but it got error of

System.InvalidCastException: Unable to cast object of type
  'System.Collections.Generic.HashSet`1[System.Int32]' to type
  'System.Collections.Generic.HashSet`1[System.Object]'.
ca9163d9
  • 27,283
  • 64
  • 210
  • 413
  • What's `paramX`? Why should `dynamic` work? Your question is very light on details. What have you tried so far? Are you keeping in mind that once you go the dynamic/reflection route, you are stuck doing _everything_ as untyped operations, until you either cast back to a type known at compile-time, or you compile new type-safe code to execute (e.g. using `Expression`) – Peter Duniho Mar 14 '20 at 03:52
  • Updated. `hs.Add(x)`. The question is about not using `dynamic`. – ca9163d9 Mar 14 '20 at 04:06
  • Your question is still unclear. As I already pointed out, if you want compile-time safety (i.e. type-safe), then you have to know the type at compile time. The `dynamic` type allows compilation to take place at run-time, which is one way to solve the problem. Building expression trees dynamically is another way to solve the problem (it's just another way to compile code at runtime). Basically any of the mechanisms that allow you compile code at runtime will work, other mechanisms will not. **What have you tried?** – Peter Duniho Mar 14 '20 at 04:12
  • 2
    Tip: Don't use the `var` keyword unless you're using anonymous types. `var` in C# is not the same thing as saying "I don't care what type this is" (for that use `Object`). By using `var` you're not able to immediate see what the static type of `x` is. – Dai Mar 14 '20 at 04:14
  • @Dai good advice for such questions... Also indeed type of `x` in the question is `Object` :) – Alexei Levenkov Mar 14 '20 at 06:01

3 Answers3

1

Like Peter pointed out, Activator is a method of working with reflection to dynamically create objects without knowing the type at build time. Type "safety" in this method is though checking types or handle type conversion errors such as with Convert.ChangeType.

Note that DbDataReader has some helper functions to work with types such as GetFieldType and GetString which can be called instead using the column index. For example:

int myColumn = 0;
var type = reader1.GetFieldType(myColumn);
if (type == typeof(string))
   string myValue = reader1.GetString(myColumn);

It more or less boils down to some using branching statements instead of reflection to manage your types. Also, you should note that many ORMs like EntityFramework or Dapper, handle a lot of this type conversion for you.

James Santiago
  • 2,883
  • 4
  • 22
  • 29
  • The `type` from this answer + https://stackoverflow.com/questions/232535/how-do-i-use-reflection-to-call-a-generic-method + the other answer's `F` is likely OP's least type-unsafe bet. – Alexei Levenkov Mar 14 '20 at 05:59
1

It seems you're wanting to perform some kind of Set Operation (Intersection? Exclusion?) over scalar values. You can solve this problem using generics (and DbDataReader.GetFieldValue<T>) without needing to use reflection or Activator, like so:

If you intend to call this F function from call-sites where the type of the colName column is known statically (i.e. at compile time), then convert F to a generic method:

void F<T>( String colName, DbDataReader reader1, DbDataReader reader2 )
{
    Itn32 rdr1Idx = reader1.GetOrdinal( colName );
    Itn32 rdr2Idx = reader2.GetOrdinal( colName );

    HashSet<T> hashSet = new HashSet<T>();

    while( reader1.Read() && reader2.Read() )
    {
        T value1 = reader1.GetFieldValue<T>( rdr1Idx );
        T value2 = reader2.GetFieldValue<T>( rdr2Idx );

        hashSet.Add( value1 );
        hashSet.Add( value2 );
        // etc - whatever logic you want here.
    }
}

Used like so:

F<Int32>( "someIntegerColumn", rdr1, rdr2 );
F<float>( "someNumericColumn", rdr1, rdr2 );
// etc

If the Type being used can only be known at runtime (e.g. because of user-supplied SQL queries) then we'll have to use Activator, but can still use a HashSet<> indirectly by wrapping it in an adapter:

using System.Collections;

void F( String colName, DbDataReader reader1, DbDataReader reader2 )
{
    Itn32 rdr1Idx = reader1.GetOrdinal( colName );
    Itn32 rdr2Idx = reader2.GetOrdinal( colName );

    Type columnType1 = reader1.GetFieldType( rdr1Idx );
    Type columnType2 = reader2.GetFieldType( rdr2Idx );
    if( columnType1 != columnType2 ) throw new InvalidOperationException( "Columns have different types." );

    var hashSet = (IList)Activator.CreateInstance( typeof(List<>).MakeGenericType( columnType1 ) );

    while( reader1.Read() && reader2.Read() )
    {
        Object value1 = reader1.GetValue( rdr1Idx );
        Object value2 = reader2.GetValue( rdr2Idx );

        hashSet.Add( value1 ); // HashSetAdapter will cast `Object value1` to its internal `T` type.
        hashSet.Add( value2 );
        // etc - whatever logic you want here.
    }
}

class HashSetAdapter<T> : IList<T> // TODO: Implement all of System.Collections.ICollection
{
    private readonly HashSet<T> hs;

    public HashSetAdapter()
    {
        this.hs = new HashSet<T>();
    }

    public void Add( Object value )
    {
        this.hs.Add( (T)value );
    }

    // TODO: Expose any other HashSet operations.
}
ca9163d9
  • 27,283
  • 64
  • 210
  • 413
Dai
  • 141,631
  • 28
  • 261
  • 374
  • The Type being used can only be known at runtime. It seems `typeof(HashSetAdapter).MakeGenericType(` will get error because of `T`? – ca9163d9 Mar 14 '20 at 05:03
  • @ca9163d9 sorry that was a typo at my end. I’ve fixed it. – Dai Mar 14 '20 at 05:36
  • `hashSet.Add( value1 )` has compiler error of `'ICollection' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type`. Changing it to `IList` and `List` works. – ca9163d9 Mar 14 '20 at 21:44
0

Here is the version using reflection.

var x = 1;

var type = x.GetType();
var hs = Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(type));
// hs.Add(x); // Error
var method = hs.GetType().GetMethod("Add");

method.Invoke(hs, new object[] { x });
method.Invoke(hs, new object[] { x + 1 });

It's slower than dynamic if .Invoke() needs to be called millions of times. dynamic seems need more time to setup.

ca9163d9
  • 27,283
  • 64
  • 210
  • 413