0

To access ValueTuple members, I would do

public Plane Init (ValueTuple<Vector4D, Vector4D, Vector4D, Vector4D> vertices)
{
    Vector4D[] m_vertices = new Vector4D [4];
    m_vertices[0] = vertices.Item1;
    // etc
    return this;
}

Can I use a nullable ValueTuple argument, and how would I access its members? Here's what I tried, and it doesn't work:

public Plane Init (ValueTuple<Vector4D, Vector4D, Vector4D, Vector4D>? vertices = null)
{
    Vector4D[] m_vertices = new Vector4D [4];
    // not working
    m_vertices[0] = vertices ?? vertices.Item1 : CVector4D (0,0,0,0);
    // not working either
    m_vertices[0] = vertices ?? vertices?.Item1 : CVector4D (0,0,0,0);
    // etc
    return this;
}
Razzupaltuff
  • 2,250
  • 2
  • 21
  • 37
  • 5
    Does `vertices?.Item1 ?? new CVector4d(0, 0, 0, 0)` work? – Lasse V. Karlsen Oct 19 '21 at 12:38
  • 1
    syntactically: do you mean `vertices.HasValue ? vertices.Value.Item1 : new CVector4D (0,0,0,0)` ? (the `:` is used in conditional expressions, which suggests the `??` should be a `?`, and you'd need a boolean predicate; however, @Lasse's null-coalescing approach is probably cleaner) – Marc Gravell Oct 19 '21 at 12:40
  • 1
    Btw, it's more common to specify valuetuples as e.g. `Init((Vector4D x1, Vector4D x2, Vector4D x3, Vector4D x4)? vertices)`, then access with `vertices.x1` or `vertices?.x1` etc – canton7 Oct 19 '21 at 12:40
  • 4
    _"it doesn't work"_ - usually if you want some help it is better to explain what fails and how instead of simply declaring that something is not working. – Guru Stron Oct 19 '21 at 12:41
  • 1
    to be honest, if this was me, I'd have two methods: one that doesn't take a parameter and defers to the other, with the options I want i.e. `Init() => Init((zero, zero, zero, zero))`, and one that takes a non-nullable parameter, i.e. `Init(ValueTuple vertices)` – Marc Gravell Oct 19 '21 at 12:42
  • @Marc: I want to call Init from a parameterless c'tor and one with parameters, because Init does a lot more than just fill a vertex vector. – Razzupaltuff Oct 19 '21 at 12:43
  • 3
    @Razzupaltuff I can't actually disagree with Guru's point, though; being very explicit about what does or doesn't happen is always better than saying "it doesn't work" – Marc Gravell Oct 19 '21 at 12:43
  • @Razzupaltuff what you're saying is "C and C# have different semantics". Yes, yes they do. And one of those semantic differences is that you can't test a reference/pointer as a boolean implicitly: OK. Note that if you *really want that*, you can actually overload the true/false operator on a specific reference-type - for example: (too long, needs another comment) - but: `Nullable` (aka `T?`) *does not implement this*, so: you'd need to test `.HasValue` or `is null`, `is not null`, etc (they compile equivalently) – Marc Gravell Oct 19 '21 at 13:02
  • https://sharplab.io/#v2:EYLgtghglgdgPgAQEwEYCwAoTA3CAnAAgHtgArAgXgIDEiiAKASgG5MoAzA+k0xzAb0wFhBBCgCc9AEQwiAdwJQALgTlE8AawDOUlpgC+OfAQDGlYmQIB+AgBYkBEAQAcAZlZYMYgGwEA8tgApngANkQQACaBEQBCdCGBEDAAcgCuISE0dExCIhQAfARKABZ48gQwgQrJREoAkmAADglggTBK0QCiAB4mgY1KUEQwTB6YyP5BoeFRsfGJKekhArnCAPRrqsrFRcWBBOxEGfKwAOYErSVEEVoANKr7JklFeIkqJfuvnBBaBBAEwHmzw6WiUHhEBA2WyuqXeezA9zk+1OgRUAGEAMoABixSHExBgu32AHIOFweIxiX8YBECMSeNZiatRK5RChfICjsRGsEIEp1C9UoF6AFgmFItE4kcFmkMkVGAR+KIAOxFRS/WQqGBLZgEQwYCEIVk+AHxbm8/mEdgQEJaYWi6YSubSpKyzJKBVKhCqlRQDU6vUGTDBjBAA== – Marc Gravell Oct 19 '21 at 13:03
  • @Marc: I am very well aware of that. I have actually written some C++ classes that use a "null" or "none" member to return "invalid" values (when a (container) class member function would return a value or value reference, but the requested value wasn't contained in it). I understand the concept behind it, but even with what I did in C++, you'd need to write way less and less obscure code and wouldn't need additional language elements ("?"). I don't want to start a religious war about which language is better; it's just my personal opinion. To each his own. :-) – Razzupaltuff Oct 19 '21 at 13:33
  • @Razzupaltuff oh, indeed; I'm not saying anything about better - just, there are differences, and those differences may impact how some things work. As a fun thing: if you accessed `var x = vertices.GetValueOrDefault();` - that *gives you what you want either way* - I should add that as an answer... – Marc Gravell Oct 19 '21 at 13:40

4 Answers4

1

To access an item for a Nullable<ValueTuple<>> you should use the Null-conditional operators ?. operator, and the null-coalescing operator ?? to provide a value in case of a null :

var v1 = vertices?.Item1 ?? fallBackValue;

But to avoid multiple redundant null test, you should use a default value for the tuple if it is null :

private static Vector4D ZeroVector4D { get; } = new Vector4D();
private static (Vector4D, Vector4D, Vector4D, Vector4D) DefaultVertices { get; } = (ZeroVector4D, ZeroVector4D, ZeroVector4D, ZeroVector4D);

public Plane Init(ValueTuple<Vector4D, Vector4D, Vector4D, Vector4D>? vertices = null)
{
    return Init(vertices ?? DefaultVertices);
}

public Plane Init(ValueTuple<Vector4D, Vector4D, Vector4D, Vector4D> vertices)
{
    m_vertices[0] = v.Item1;
    m_vertices[1] = v.Item2;
    m_vertices[2] = v.Item3;
    m_vertices[3] = v.Item4;

    return this;
}

You can also use some pattern matching :

var m_vertices = new Vector4D[4];
if (vertices is (Vector4D a, Vector4D b, Vector4D c, Vector4D d))
{
    m_vertices[0] = a;
    m_vertices[1] = b;
    m_vertices[2] = c;
    m_vertices[3] = d;
}
Orace
  • 7,822
  • 30
  • 45
1

By using the concise C# syntax for ValueTuple types and values, we can write:

static readonly Vector4D Origin4D = new Vector4D(0, 0, 0, 0);

public Plane Init((Vector4D, Vector4D, Vector4D, Vector4D)? vertices)
{
    var v = vertices ?? (Origin4D, Origin4D, Origin4D, Origin4D);
    var m_vertices = new Vector4D[] { v.Item1, v.Item2, v.Item3, v.Item4 };

    // etc
    return this;
}

If you are in control of Vector4D it is a good idea to add it a static member

public static Vector4D Origin { get; } = new Vector4D(0, 0, 0, 0);

You can then access it with Vector4D.Origin.


If you need a variable number of parameters, use a params parameter:

public Plane Init(params Vector4D[] vertices)
{
    Vector4D[] m_vertices = vertices;
}
Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • 1
    Wish is simply : `var v = vertices ?? (Origin4D, Origin4D, Origin4D, Origin4D);` – Orace Oct 19 '21 at 13:10
  • Yeah, thanks man. I have to suppress laughter (not at you, but at C#). I had hoped for a language that would offer most of the strengths of C++ while taking away most of its pains. In C++, I would just pass an arbitrary number of arguments with std::initializer_list and check its length ... I could also use std::tuple and r/w access the ith tuple element with std::get(). – Razzupaltuff Oct 19 '21 at 13:10
  • 1
    @Razzupaltuff you are looking for `params` : https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/params and by the way this become a XY question – Orace Oct 19 '21 at 13:12
  • @Orace, yes of course! I also thought of the `params` parameter; however, we must have exactly 4 vectors. – Olivier Jacot-Descombes Oct 19 '21 at 13:16
  • I don't need a variable number of params here. – Razzupaltuff Oct 19 '21 at 13:27
  • Using a static default member like CVector4D.Origin would mostly help with performance, right? Nice idea though. – Razzupaltuff Oct 19 '21 at 13:29
  • A static member like `Origin` helps in performance and reusability. It also helps in writing shorter and more readable code. – Olivier Jacot-Descombes Oct 19 '21 at 13:50
0

I found a solution that comes closest to what I want:

public Plane Init (ValueTuple<CVector4D, CVector4D, CVector4D, CVector4D>? vertices = null)
{
    CVector4D[] m_vertices;
    m_vertices = new Vector4D[4];
    // the elements of m_vertices are default initialized as intended already here
    if (vertices != null)
    {
        m_vertices[0] = vertices?.Item1;
        m_vertices[1] = vertices?.Item2;
        m_vertices[2] = vertices?.Item3;
        m_vertices[3] = vertices?.Item4;
    }
    return this;
}

It is just a little guesswork and a heck of a lot of googling to find out how to write this down.

Btw,

m_vertices[0] = vertices?.Item1 ?? vertices?.Item1 : new Vector4D (0,0,0,0);

Does not work; Instead of ":", I get an error that ";" or "}" are expected.

Simply writing

m_vertices[0] = vertices.Item1;

after the "if (vertices != null)" statement yields an error as well: (Vector4D, Vector4D, Vector4D, Vector4D) does not contain a definition for 'Item1' and no accessible extension method 'Item1' accepting a first argument of type '(Vector4D, Vector4D, Vector4D, Vector4D)' could be found.

Using target system ".NET Framework 4.7.2".

With the hint of Orace below, this is probably the (or a) right way to do it:

public Plane Init (ValueTuple<CVector4D, CVector4D, CVector4D, CVector4D>? vertices = null)
{
    CVector4D[] m_vertices;
    m_vertices = new Vector4D[4];
    // the elements of m_vertices are default initialized as intended already here
    if (vertices != null)
    {
        m_vertices[0] = vertices.Values.Item1;
        m_vertices[1] = vertices.Values.Item2;
        m_vertices[2] = vertices.Values.Item3;
        m_vertices[3] = vertices.Values.Item4;
    }
    return this;
}
Razzupaltuff
  • 2,250
  • 2
  • 21
  • 37
  • You don't need question marks here: `vertices?.Item1`. You already checked in the `if` that `vertices` is not null. – Evk Oct 19 '21 at 12:57
  • Use `vertices.Value.Item1` to avoid redundant `null`checks. – Orace Oct 19 '21 at 13:01
0

In the case where the null scenario would give the zero-default equivalent (if we assume that CVector4D is a struct), then there is a cheat you can use to simplify this code:

public Plane Init (ValueTuple<CVector4D, CVector4D, CVector4D, CVector4D>? vertices = null)
{
    var val = vertices.GetValueOrDefault();
    m_vertices = new[] { val.Item1, val.Item2, val.Item3, val.Item4 };
    // ...
}

This works because GetValueOrDefault() will return the inner T value of the T? if there is a value, and the default(T) otherwise; and in this case: default(T) will be the all-zero case that you wanted. So: all done.

But: you can probably simplify a bit more, and lose the T?:

public Plane Init (ValueTuple<CVector4D, CVector4D, CVector4D, CVector4D> vertices = default)
{
    m_vertices = new[] { vertices.Item1, vertices.Item2, vertices.Item3, vertices.Item4 };
    // ...
}

That said: it may be clearer to pass in the vectors individually rather than as a value-tuple, i.e.

public Plane Init (CVector4D vertex0, CVector4D vertex1, CVector4D vertex2, CVector4D vertex3)
{
    m_vertices = new[] { vertex0, vertex1, vertex2, vertex3 };
    // ...
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900