I have problem with understanding generic method in interface. I'm using .NET framework 4.8 with auto C# version so it should be C# 7.3. Example below
Interface is:
public interface IRegister
{
Nullable<T> Read<T>() where T: struct;
void Write<T>(T value) where T : struct;
}
Now some class inherit from interface:
public abstract class AbstractRegister : IRegister
{
protected ModbusClient client;
protected int Address;
public abstract Nullable<T> Read<T>() where T : struct;
public virtual void Write<T>(T value) where T : struct
{
return;
}
public AbstractRegister(int address, ModbusClient client)
{
this.client = client;
this.Address = address;
}
}
Now I have some class inherited from AbstractRegister:
class InputRegister_int : AbstractRegister
{
public InputRegister_int(int address, ModbusClient client) : base(address, client) { }
public override int? Read<int>()
{
if (client == null)
return null;
return this.client.ReadInputRegisters(this.Address, 1)[0] as int?;
}
}
and in line public override int? Read<int>()
I have errors:
CS1001: Identifier expected
CS1003: Syntax error, '>' expected
CS1003: Syntax error, '(' expected
I'm using Visual Studio Community 2022
When I use Int32 instead int, everything works ok, and it almost resolve my problem, but type float (which is necessary for me) doesn't have Float representation.
I have no ideas for solving this problem anymore. Can someone explain it to me?
CodePudding user response:
Perhaps try changing the interface to being generic, that way the generic type references are constrained to being of the same type, and not just arbitrarily any type:
public interface IRegister<T>
where T : struct
{
Nullable<T> Read();
void Write(T value);
}
public abstract class AbstractRegister<T> : IRegister<T>
where T : struct
{
protected ModbusClient client;
protected int Address;
public abstract Nullable<T> Read();
public virtual void Write(T value)
{
return;
}
public AbstractRegister(int address, ModbusClient client)
{
this.client = client;
this.Address = address;
}
}
class InputRegister_int : AbstractRegister<int>
{
public InputRegister_int(int address, ModbusClient client) : base(address, client) { }
public override int? Read()
{
if (client == null)
return null;
return this.client.ReadInputRegisters(this.Address, 1)[0] as int?;
}
}
class InputRegister_float : AbstractRegister<float>
{
public InputRegister_float(int address, ModbusClient client) : base(address, client) { }
public override float? Read()
{
if (client == null)
return null;
return this.client.ReadInputRegisters(this.Address, 1)[0] as float?;
}
}
The functional difference here is that specifying the generic type at the class level, all of the T's in the base class MUST be the same so for InputRegister_int
all the T's a re now int
.
From a syntax perspective, notice that the methods DO NOT have the suffix on their prototypes, this is because they are not generic methods at all, they are specifically typed to the same type parameter that was specified when the instance was created.
If you did have a method like this:
Nullable<T> Read<T>() where T: struct;
Then even to the InputRegister_int
class we could pass through ANY type of struct to that method and expect it to return the value, the following would be expected to be valid at runtime:
var intRegister = new InputRegister_int();
int intValue = intRegister.Read<int>();
DateTime dtValue = intRegister.Read<DateTime>();
If you really do want your register to be any arbitrary type, then we don't really need an abstract register at all:
public class GenericRegister : IRegister
{
protected ModbusClient client;
protected int Address;
public virtual Nullable<T> Read<T>() where T : struct
{
if (client == null)
return null;
return this.client.ReadInputRegisters(this.Address, 1)[0] as T?;
}
public virtual void Write<T>(T value) where T : struct
{
// TODO: implement generic write logic
return;
}
public AbstractRegister(int address, ModbusClient client)
{
this.client = client;
this.Address = address;
}
}
However this leads to a very lazy implementation, the types of the values in your ModBus registers are not going to change, most are fixed to hardware implementations but the internal logic in the PLC will contrain the types for you. Your fixed type InputRegister_int
allows you do to specific value and type checking on the response from the client when you need to and it helps guide the developers to make reasonable decisions later.
CodePudding user response:
This answer is specifically about "Int32" vs. int
part of the question.
Int32
used in the code as example of "this works" is not actually System.Int32
(which is exactly what int
is) but rather just a name that happen to match name of some system type.
Code below shows simpler example of this - you can see that "Int32" behaves exactly as any other string that is not a type name.
using System;
public class SomeClass {
// exactly the same as T X<T>(), just using `Int32` as type parameter name
Int32 X<Int32>() { return default(Int32); }
// fails to compile as `System.Int32` and `int` are concrete types
System.Int32 Y<System.Int32>() { return default(System.Int32); }
int X<int>() { return default(int); }
}
Conventionally name of the type parameter in generic method starts with T
(just T
or prefixed with it - TResult
), but there is rule in C# specification that enforces that. As result anything that is not existing type name is considered "name of the type parameter" as long as it is used in the right place void M<AnythingThatIsNotType>(...)
.