5

i was wondering if there is a way to restrict a value in construction. Here is my code :

class Student : Human 
{
    private double Grade;

    public Student(string FirstName, string LastName, double Grade)
        : base(FirstName, LastName)
    {
        this.FirstName = FirstName;
        this.LastName = LastName;
        this.Grade = Grade;
    }
}

and when i make a new Student i want to restrict the Grade between >= 2.00 and <= 6.00 , like compile error or excepion on runtime. Is there a way ? (Don't worry about other fields FirstName, and LastName)

svick
  • 236,525
  • 50
  • 385
  • 514
Yoan Dinkov
  • 531
  • 1
  • 5
  • 17
  • Don't think you can make it a compile time error, but, going on the assumption that `Grade` is user input, you can run a check to make sure it's within your range, and then let the user know if that fails. – PiousVenom Mar 15 '13 at 13:37
  • Do you really want *double*? or do you want *decimal*? Remember, double cannot precisely represent fractions that are not powers of two on the denominator. – Eric Lippert Mar 15 '13 at 15:24

7 Answers7

12

You can check it and throw an exception at runtime, like this:

if (grade < 2.00 || grade > 6.00)
    throw new ArgumentOutOfRangeException("grade");

Always put such conditions at the start of the method or constructor. I even put them in their own #region (but that's my personal preference):

public Student(string firstName, string lastName, double grade)
    : base(firstName, lastName)
{
    #region Contract
    if (grade < 2.00 || grade > 6.00)
        throw new ArgumentOutOfRangeException("grade");
    #endregion
    this.FirstName = firstName;
    this.LastName = lastName;
    this.Grade = grade;
}

However, there is a way to get compile-time warnings for such things using Code Contracts. You can download Code Contracts on MSDN and there you can also find the documentation. It only integrates with non-Express versions of Visual Studio and is written by Microsoft. It will check whether method calls are likely to adhere to the contract you specify. Your code would then become:

using System.Diagnotistics.Contracts;

public Student(string firstName, string lastName, double grade)
    : base(firstName, lastName)
{
    #region Contract
    Contract.Requires<ArgumentOutOfRangeException>(grade >= 2.00);
    Contract.Requires<ArgumentOutOfRangeException>(grade <= 6.00);
    #endregion
    this.FirstName = firstName;
    this.LastName = lastName;
    this.Grade = grade;
}
Daniel A.A. Pelsmaeker
  • 47,471
  • 20
  • 111
  • 157
  • +1 for ArgumentOutOfRangeException, but disagree with the region. – ken2k Mar 15 '13 at 13:39
  • Why not combine into one Requires clause? `Contract.Requires(grade >= 2.00 && grade <= 6.00);` – Andy Mar 15 '13 at 13:47
  • @Andy Code Contracts used to be bad at dealing with compound requires/ensures and invariants. I'm not sure whether that's still the case, but in the interests of that and readability (long lines and such) I split them up. – Daniel A.A. Pelsmaeker Mar 15 '13 at 13:48
4

You can thrown an exception:

class Student : Human 
{
    private double Grade;

    public Student(string FirstName, string LastName, double Grade)
        : base(FirstName, LastName)
    {
        if (Grade >= 2 && Grade <= 6) { 
          throw new ArgumentOutOfRangeException();
        }

        this.FirstName = FirstName;
        this.LastName = LastName;
        this.Grade = Grade;
    }
}

If you're using Microsoft Code Contracts you can do it this was as well:

class Student : Human  {
    private double Grade;

    public Student(string FirstName, string LastName, double Grade)
        : base(FirstName, LastName)
    {
        System.Diagnotistics.Contracts.Contract.Requires<ArgumentOutOfRangeException>(Grade >= 2 && Grade <= 6);

        this.FirstName = FirstName;
        this.LastName = LastName;
        this.Grade = Grade;
    }
}
Andy
  • 8,432
  • 6
  • 38
  • 76
  • The condition in your first example is inverted. It throws on grades between 2 and 6 inclusive instead of throwing except on those grades. – Redwood Mar 20 '13 at 21:18
1

There's another option, that might be a bit combersome but will ensure the given ranges, is not to use double at all, but create a custom class called Grade. Your Grade class will contain casts from and to double, and will enforce validation. This means that all Grade validation logic is in the Grade struct, and the Student class just receives a Grade object and doesn't have to worry about it.

public struct Grade
{
    public static readonly double MinValue = 2.0;
    public static readonly double MaxValue = 6.0;

    private double value;

    public static explicit operator double(Grade grade)
    {
        return grade.value + MinValue;
    }

    public static explicit operator Grade(double gradeValue)
    {
        if (gradeValue < MinValue || gradeValue > MaxValue)
            throw new ArgumentOutOfRangeException("gradeValue", "Grade must be between 2.0 and 6.0");

        return new Grade{ value = gradeValue - MinValue };
    }
}

And you would use it like this:

double userInput = GetUserInputForGrade();
Grade grade = (Grade)userInput; // perform explicit cast.
Student student = new Student(firstName, lastName, grade);

Edit: I've updated the code with @EricLippert's suggestions, to make the class expose its min/max values to a developer.

Edit 2: Updated again with @JeppeStigNielsen's suggestion. The value field now stores an offset from MinValue, so that a call to default(Grade) will return a valid value (equal to MinValue) regardless of whether 0 is inside the valid range.

Avner Shahar-Kashtan
  • 14,492
  • 3
  • 37
  • 63
  • But then you lose all operators, all comparison and equality methods, and any method accepting a `double` will no longer accept your `Grade`. – Daniel A.A. Pelsmaeker Mar 15 '13 at 13:49
  • @Virtlink: So implement those operations on the type Grade if you miss them. – Eric Lippert Mar 15 '13 at 13:55
  • 1
    I like this idea but I would make the constants *public static readonly fields*. Public because we want the user to be able to know when a double is out of range. Static because it's type-wide. And readonly, not constant, because the range might change in the future. – Eric Lippert Mar 15 '13 at 13:57
  • 4
    @EricLippert This struct has the problem that `default(Grade)` (or equivalently `new Grade()`) will create an "illegal" value `0.0` of the `Grade` struct. Your check is not run in that case. One work-around is to store the ***excess*** compared to `2.0` (`MinValue`) in the instance field. So for exapmle if the real grade was `2.5`, you would store `0.5`. Your conversion operators would then have to add (respectively subtract) `MinValue` before they `return`. – Jeppe Stig Nielsen Mar 15 '13 at 14:24
  • @JeppeStigNielsen: Excellent point! It is important to remember that the default value of a struct is always a legal value. – Eric Lippert Mar 15 '13 at 15:22
0
class Student : Human 
{
    private double Grade;

    public Student(string FirstName, string LastName, double Grade)
        : base(FirstName, LastName)
    {
        if (Grade < 2 || Grade > 6)
            throw new ArgumentOutOfRangeException("Grade must be between 2 and 6");

        this.FirstName = FirstName;
        this.LastName = LastName;
        this.Grade = Grade;
    }
}
Khan
  • 17,904
  • 5
  • 47
  • 59
0
public Student(string FirstName, string LastName, double Grade)
    : base(FirstName, LastName)
    {
     if(Grade >= 2.0 || Grade <= 6.00)
      throw  new ArgumentException("your message");     
    }
Tom Squires
  • 8,848
  • 12
  • 46
  • 72
0

If you want any chance of compile time cheking you have to use code contracts. They are essentially exceptions at runtime, so equivalent to argument validations with exceptions.

public Student(string firstName, string lastName, decimal grade)
{
    Contract.Requires(grade >= 2);
    Contract.Requires(grade <= 6);

    FirstName = firstName;
    LastName = lastName;
    Grade = grade;     
}

See this question

Design by contracts and constructors

Community
  • 1
  • 1
Anders Forsgren
  • 10,827
  • 4
  • 40
  • 77
-2
class Student : Human 
{
    private double Grade;

    public Student(string FirstName, string LastName, double Grade)
        : base(FirstName, LastName)
    {
        this.FirstName = FirstName;
        this.LastName = LastName;
        this.Grade = Grade;
        if(Grade >= 2 and Grade <= 6){
            throw new Exception("Incorrect grade input");
    }
}

Or throw the relevent exception you want to e.g. ArgumentOutOfException etc...

SpaceApple
  • 1,309
  • 1
  • 24
  • 46
  • The exception is up to the programmer, and since we don't have enough information on why the grades should not be between 1 and 7 should not mean a specific exception should be thrown e.g. ArgumentOutOfException. – SpaceApple Mar 15 '13 at 13:42
  • @SpaceApple There's a general rule that you never just throw Exception. It goes against .Net best practices. – Andy Mar 15 '13 at 13:43
  • True. ArgumentException is not valid in this case as well since it might not meet the programmers needs. But I do understand your point. – SpaceApple Mar 15 '13 at 13:47