First let's define the main accessibility options in C#.
- public: anyone has access
- private: only the owning type has access
- protected: only the owning type and derived types have access
- internal: only the assembly containing the definition has access
Accessibility has nothing to do with what types/members it is applied to.
1) Root types can only be public or internal. Protected and private don't make sense because these limit the type/member to the owning type and root types don't have one. Therefore C# doesn't allow these accessibilities. However if the type is nested in another type then it can be any accessibility.
csharp
//Root types can be public or internal only
public class PublicClass {} // Accessible by anyone
internal class InternalClass {} // Accessible to assembly
//protected class ProtectedClass {} //Compiler error - no owning type
//private class PrivateClass {} // Compiler error - no owning type
class SomeClass
{
public class PublicClass {} // Accessible by anyone
internal class InternalClass {} // Accessible to assembly
protected class ProtectedClass {} // Accessible to owning and derived types
private class PrivateClass {} // Accessible to owning type only
}
2) In general you should use a struct for small sets of related data. Structs should be immutable so once created you shouldn't be able to modify the data. The original guideline was around 64 bytes but that is just a suggestion and ref structs (added in C# 9) change the rules a little.
structs, like all value types, cannot inherit from a custom type nor be inherited from. If you need to be able to create derived types or use a custom base type then you must use a class. Small classes are fine. The overhead is just when allocating and cleaning them up. Options, for example, tend to start out small and may be derived from so they tend to be classes. Things like an address or lat/long or name often contain multiple data points but don't make sense unless combined together so they can be a struct.
So, in summary, if you need to inherit from a type then you must use a class. However this has nothing to do with using the type as a field in another type. If you want a base type to expose a property to derived types then use either a class or a struct. If the derived type must be able to set the property then ensure the setter is accessible to the derived type. If it is a struct type then it should be immutable so derived types would need to create a new instance. C# does not allow you to set the properties of a struct type stored in a property.
csharp
public struct LatLong
{
public LatLong ( double lat, double lon ) : this()
{
Latitude = lat;
Longitude = lon;
}
public double Latitude { get; }
public double Longitude { get; }
}
class SomeType
{
//Anybody can get latlong but cannot change it
//Derived types can set a new latlong but not change existing one
public LatLong Location { get; protected set; }
}
var instance = new SomeType();
var ll = instance.Location; //Anybody can get value
//ll.Latitude = ll.Longitude = 10.0; //Compiler error, properties are read only as they should be
//instance.Location = new LatLong(); //Compiler error, setter is protected
class DerivedType: SomeType
{
public void SetLocation ( LatLong value )
{
//Allowed because derived types can set
Location = value;
//Compiler error - properties are read only as they should be
//Location.Latitude = value.Latitude;
//Location.Longitude = value.Longitude;
}
}
Note that creating a well formed value type requires a lot more effort because you should implement equality, comparison, etc. Refer to MSDN docs for how to create a "correct" value type. But for simple types you can get away without it.
Think of a value type as a single value that you can either get or set just like integrals or floats. The fact that it might have several child values that make up the whole value isn't relevant. The value type itself is "atomic". Classes don't work that way though so they are more flexible, if you need it.