Runtime: ValueTuple can make writing and reading data from streams easier

Created on 4 Jul 2018  路  32Comments  路  Source: dotnet/runtime

Suppose I have this code:

var S = File.Open("C:\\1.txt", FileMode.OpenOrCreate);
var BW = new BinaryWriter(S);
BW.Write("Ali");
BW.Write(15);
BW.Write('a');
BW.Write(100M);
BW.Close( );

If BinaryWriter.Write method can receive a ValueTuple param, then we can rewrite the code as this:

var S = File.Open("C:\\1.txt", FileMode.OpenOrCreate);
var BW = new BinaryWriter(S);
BW.Write(("Ali", 15, 'a', 100M));
BW.Close();

Same for BinaryReader.. If we have this code:

var S = File.Open("C:\\1.txt", FileMode.OpenOrCreate);
BinaryReader BR = new BinaryReader(S);
Console.WriteLine(BR.ReadString( ));
Console.WriteLine(BR.ReadInt32( ));
Console.WriteLine(BR.ReadChar( ));
Console.WriteLine(BR.ReadDecimal( ));
BR.Close( );

if BinaryReader.Read method can receive an out ValueTuple param, we can rewrite the code like this:

var S = File.Open("C:\\1.txt", FileMode.OpenOrCreate);
var BR = new BinaryReader(S);
BR.Read(out (string, int, char, decimal) values)
Console.WriteLine(values.ToString());
BR.Close();

This can be done with all streams.

api-suggestion area-System.IO

Most helpful comment

@MohammadHamdyGhanem

I think explicit interface implementation is ued only when implementing two interfaces that have the same member, so calling this member directly will be confusing

No, it's used whenever the author of a type wants to use it. Exposing additional Length and indexer properties from a concrete tuple type where length and item properties are known doesn't make a lot of sense, so hiding those interface members makes perfect sense.

All 32 comments

The implementation of this would be expensive in a variety of ways. It's not worth it.

This is also something that extension methods would be just as good at.

Wouldn't it require an exponential number of overloads?

Wouldn't it require an exponential number of overloads?

If concrete types were used, yes, leading to a very expensive set of APIs due to quantity.

If generics were used, then the implementation would need to type test all possible types that are supported for each generic, mapping each to the corresponding write or read method. And each instantiation that involved a value type would lead to bloated asm.

Hence my "expensive in a variety of ways" comment.

I don't think extension methods are good at any thing, and can,t imagine I had an extra class for each. Net class or one huge class containing all this nonsense methods. I never used these extension methods, exept those built in dot net for linq, and I will never do.
Besides, I prefere to use Vb.NET file methodes suplied by MYy.Computer.FileSystem object. I advice you to make use of those and add them to corefx, especially the TextFieldParser class, which is the nearest thind to vb6 file records. It will be nice if we can save fixed length structured and read them back as one block (I know that is can be done by serialization, but in vb6 it is saved as a binary dsta with no extra info, you just read data in a fixed length struct and vb read bytes and convert them into the data types of each Field).
Using Value tuple can elimunate the need to predefined structs, and make use of existing file classes. We just call the write methode to save each item in tuple.

@stephentoub
I suggest the base ValueTuple class be modified by adding an Items vertual property, so we can use the base class as a generic type parameter that can accept any tuple, and enumerate through its items. Acually I was thinnking this is already doable, and I am surprised it is not!

the base ValueTuple class

There is no base class. The various ValueTuple<> types are structs.

so we can use the base class as a generic type parameter that can accept any tuple

Using an interface for that would require boxing every value type that was yielded (and potentially the ValueTuple<> itself), since it'd be defined as yielding objects to accomodate any possible type. That's yet another expense.

@MohammadHamdyGhanem

I don't think extension methods are good at any thing

LINQ would beg to differ

Besides, I prefere to use Vb.NET file methodes

Then use them. Either reference the VB.NET assemblies from C# and use them, or use VB.NET.

As mentioned, trying to support tuples here would lead to an explosion of overloads, each of which would have to negotiate the type parameters in order to figure out how to write the given type. It's not worth doing.

I suggest the base ValueTuple class be modified by adding an items property,

All ValueTuple structs implement ITuple.

But using that to try to come up with a single overload would involve boxing all value type members and still negotiating the type of each value. It's still ugly. Go write your own.

@stephentoub
Unfortunately, The way ypou implemented Tuples made many problems!
But how can the primitive VB6 save any structure to the file and re-read it, while .net can't?
There should be some way to do this in .NET. (And apply that on tuples as well).

@HaloFour
ITuple has only the Length property, and I donn't know how to get the items and thier types. I agree this will not be easy.

@MohammadHamdyGhanem

And an indexer which returns the value.

Then use them. Either reference the VB.NET assemblies from C# and use them, or use VB.NET.

This is not available in CoreFx. My object is part of VB.NET on .NET Framework only, so I asked more than once (as in some VB.NET trace classes) to include the unique functionality provided by this object in CoreFx and make it available to alll languages.

@HaloFour

And an indexer which returns the value.

Thanks. This If there is Ituple.GetType(int index) method, things will be easier. To pat price of boxing or not, is a decision of the programmer.

@HaloFour
Seems you made a good work to hide the ITuple interface!
this will not work:

var z = (2, "d");
Console.WriteLine(z[0]);

but this will:

var z = (2, "d");
Console.WriteLine( ((ITuple)z)[0] );

Strange!

@MohammadHamdyGhanem

It's called "explicit interface implementation" and it's been a part of .NET since 1.0.

@HaloFour
I think explicit interface implementation is ued only when implementing two interfaces that have the same member, so calling this member directly will be confusing. What is the purpose of this here?
Iterating through a tuple can be useful in some situations, and I was looking for a way to do that (thanks by the way), so why hide this info?

@MohammadHamdyGhanem

I think explicit interface implementation is ued only when implementing two interfaces that have the same member, so calling this member directly will be confusing

No, it's used whenever the author of a type wants to use it. Exposing additional Length and indexer properties from a concrete tuple type where length and item properties are known doesn't make a lot of sense, so hiding those interface members makes perfect sense.

@MohammadHamdyGhanem if you absolutely want to do this in this way, you could leverage your beloved extension methods (:wink:) to create something like this:

using System;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                BinaryWriter bw = new BinaryWriter(ms);
                bw.Write(("Ali", 15, 'a', 100M));
            }
        }
    }

    public static class BinaryWriterExtensions
    {
        public static void Write(this BinaryWriter bw, ITuple tuple)
        {
            Type type = tuple.GetType();
            Type[] itemTypes = type.GetGenericArguments();
            Type binaryWriterType = typeof(BinaryWriter);

            for (int i = 0; i < tuple.Length; ++i)
            {
                object item = tuple[i];
                Type itemType = itemTypes[i];

                MethodInfo methodInfo = binaryWriterType.GetMethod("Write", new Type[] { itemType });
                methodInfo.Invoke(bw, new object[] { item });
            }
        }
    }
}

But this relies on reflection, hence is inheritely unperformant and shouldn't find it's way into corefx.
Note this snippet has no argument validation, is unoptimized, etc. Just to give you an idea...

A total different approach would be that the compiler "expands" the valuetuple into separate calls to the BinaryWriter (just as you do manually). This might work, but I doubt it's worth the effort.

By compiler I don't mean explicit Roslyn. There are other ways to "compile" this, e.g. via IL-generation at runtime (sou you could create a library that mimics the VB6 behaviour), Roslyn analyzer that "expands" this for you, etc.

@gfoidl
Thanks.
This can be written without reflection like this:

    public static void Write(this BinaryWriter bw, ITuple tuple)
    {
        for (int i = 0; i < tuple.Length; ++i)
        {
            object item = tuple[i];
            if (item is int)
                bw.Write((int)item);
            else if (item is byte)
                bw.Write((byte)item);
            else if (item is short)
                bw.Write((short)item);
            else if (item is long)
                bw.Write((long)item);
            else if (item is double)
                bw.Write((double)item);
            else if (item is double)
                bw.Write((double)item);
            else if (item is float)
                bw.Write((float)item);
            else if (item is char)
                bw.Write((char)item);
            else if (item is bool)
                bw.Write((bool)item);
            else if (item is decimal)
                bw.Write((decimal)item);
            else if (item is byte[])
                bw.Write((byte[])item);
            else if (item is char[])
                bw.Write((char[])item);
            else if (item is string)
                bw.Write((string)item);
            else if (item is ulong)
                bw.Write((ulong)item);
            else if (item is uint)
                bw.Write((uint)item);
            else if (item is ushort)
                bw.Write((ushort)item);
            else if (item is sbyte)
                bw.Write((sbyte)item);
        }
    }

I wish there is a amart way to convert objects to thier underline types in runtime.. sothing like bw.write((exacttype)item). This will reduce the above code to:

    public static void Write(this BinaryWriter bw, ITuple tuple)
    {
        for (int i = 0; i < tuple.Length; ++i)
              bw.Write((exacttype)tuple[i]);
   }            

I think C# can do this, and it will optimize some CoreFx classes codes.

@MohammadHamdyGhanem

I wish there is a amart way to convert objects to thier underline types in runtime

They're already their underlying types at runtime.

If you're asking for the CLR to do overload resolution at runtime, that's a terrible and fragile idea. The CLR is not some kind of dynamic language.

If you are going horribly box; might as well use pattern matching to the full and drop the second cast

public static void Write<TTuple>(this BinaryWriter bw, TTuple tuple)
        where TTuple : ITuple
{
    for (int i = 0; i < tuple.Length; ++i)
    {
        switch (tuple[i])
        {
            case int si:
                bw.Write(si);
                break;
            case byte by:
                bw.Write(by);
                break;
            case short s:
                bw.Write(s);
                break;
            case long l:
                bw.Write(l);
                break;
            case double d:
                bw.Write(d);
                break;
            case float f:
                bw.Write(f);
                break;
            case char c:
                bw.Write(c);
                break;
            case bool bo:
                bw.Write(bo);
                break;
            case decimal dec:
                bw.Write(dec);
                break;
            case byte[] ba:
                bw.Write(ba);
                break;
            case char[] ca:
                bw.Write(ca);
                break;
            case string str:
                bw.Write(str);
                break;
            case ulong ul:
                bw.Write(ul);
                break;
            case uint ui:
                bw.Write(ui);
                break;
            case ushort us:
                bw.Write(us);
                break;
            case sbyte sb:
                bw.Write(sb);
                break;
            default:
                throw new ArgumentException("Unknown type");
        }
    }
}

Note: you are going to box each individual item to object; so it won't be great for performance or deal with types not in the list

@benaadams
That's good. We have two situations:
1- Writing data into a small file, like saving some settings or so. This code should be as short as possible, where performance impact can be neglected.
2- Writing data into large files, like videos or 3D objects, where performance is most important, so, we should optimize writing and reading operations as possible, using MemoryMappedFile or span or whatever.
I think it is a good thing to give the programmer the two sets of tools, to write short rapid code whenever he wants, and write long optimized code when it is necessary.
Thanks all.

You can also do it with if, e.g.

else if (item is byte b) bw.Write(b);

Here's the C# pattern matching docs https://docs.microsoft.com/en-us/dotnet/csharp/pattern-matching

HTH

If you don't care much about boxing and performance in general:

  1. There's no reason to use a tuple on the write side, when you could just use the simpler and more familiar params object[].
  2. To "convert objects to their underlying type" you can use dynamic: bw.Write((dynamic)item). This will perform overload resolution at runtime based on C# rules.

@svick
Nice. I wrote this extension methpod:

public static void Write(this BinaryWriter bw, ITuple tuple)
{
    for (int i = 0; i < tuple.Length; ++i)
        bw.Write((dynamic)tuple[i]);
}

and tested it with:

var BW = new BinaryWriter(S);
BW.Write(("Ali", 15, 'a', 100M));
BW.Close();

it took less than 55ms on my old PC, which will be less ofcource on a more powerful PC. I don't care even such small jobs took 2 seconds. I write such small data more often to save some settings, temp info or testing something, so I care more for making code shorter.
Note: Write method doesn't accept pbject params array, so "1" doesn't apply. But thanks for "2".

@MohammadHamdyGhanem, you can include such methods in your project. You can also publish your own NuGet library of extensions for others to consume if you believe them to be useful. We're just not going to add this to coreclr/corefx right now.

The ITuple indexer is readonly, so I can't use similar proccees with the BinaryReader.Read!

I have this:

        void Foo(out ITuple x)
        {
            x = (1, "ab");
        }

and tried this code:

var x = (0, "");
Foo(out x);

But C# says:

Error CS1503 Argument 1: cannot convert from 'out (int, string)' to 'out ITuple'

if I remove the out keyword from the function defention and call, everything is OK. Why?

@MohammadHamdyGhanem

  1. This isn't the C# repo.
  2. This isn't your personal blog for how you develop stuff.
  3. ref/out parameters aren't variant.

After some trials, I found that there is no easy way to use typles to read multiple values from a file. I tried another way like this:
List<object> data = Br.Read(typeof(string), typeof(int), typeof(char), typeof(decimal));
It took less than 5ms to execute which is good for me, but obviously it is not short and results are not in the easy form to use.
So, doing anything with the read methods is a lost case, unless there is away to write:
var data = Br.Read(string, int, char, decimal);
which seems nice but not valid!
thanks.

Last Note:
May be I can use this:
var data = Br.Read<(string, int, char, decimal)>();
where the read method is defined as:
public static List<object> Read<T>(this BinaryReader br) where T: ITuple

I did this for the read method:

    public static T Read<T>(this BinaryReader br) 
    {
        if (typeof(T) == typeof(int))
            return (T)(object)br.ReadInt32();
        if (typeof(T) == typeof(byte))
            return (T)(object)br.ReadByte();
        if (typeof(T) == typeof(short))
            return (T)(object)br.ReadInt16();
        if (typeof(T) == typeof(long))
            return (T)(object)br.ReadInt64();
        if (typeof(T) == typeof(double))
            return (T)(object)br.ReadDouble();
        if (typeof(T) == typeof(float))
            return (T)(object)br.ReadSingle();
        if (typeof(T) == typeof(char))
            return (T)(object)br.ReadChar();
        if (typeof(T) == typeof(bool))
            return (T)(object)br.ReadBoolean();
        if (typeof(T) == typeof(decimal))
            return (T)(object)br.ReadDecimal();
        if (typeof(T) == typeof(string))
            return (T)(object)br.ReadString();
        if (typeof(T) == typeof(ulong))
            return (T)(object)br.ReadUInt64();
        if (typeof(T) == typeof(uint))
            return (T)(object)br.ReadUInt32();
        if (typeof(T) == typeof(ushort))
            return (T)(object)br.ReadUInt16();
        if (typeof(T) == typeof(sbyte))
            return (T)(object)br.ReadSByte();

       throw new Exception("Invalid Type param");
    }

    public static (T1, T2) Read<T1, T2>(this BinaryReader br)
            => (Read<T1>(br), Read<T2>(br));

    public static (T1, T2, T3) Read<T1, T2, T3>(this BinaryReader br)
            => (Read<T1>(br), Read<T2>(br), Read<T3>(br));

   public static (T1, T2, T3, T4) Read<T1, T2, T3, T4>(this BinaryReader br)
            => (Read<T1>(br), Read<T2>(br), Read<T3>(br), Read<T4>(br));

    public static (T1, T2, T3, T4, T5) Read<T1, T2, T3, T4, T5>(this BinaryReader br)
             => (Read<T1>(br), Read<T2>(br), Read<T3>(br), Read<T4>(br), Read<T5>(br));

Now I can write:
var data = Br.Read<string, int, char, decimal>();

Was this page helpful?
0 / 5 - 0 ratings