Throw specific exception for a violation of a contract on an automatic property

ぃ、小莉子 提交于 2019-12-11 10:21:35

问题


I can specify a contract for an automatic property like this (example taken from the CC documentation):

public int MyProperty { get; set ; }

[ContractInvariantMethod]
private void ObjectInvariant () {
  Contract. Invariant ( this .MyProperty >= 0 );
  ...
}

When runtime-checking is turned on, and an attempt is made to assign an invalid value to MyProperty, the setter throws System.Diagnostics.Contracts.__ContractsRuntime+ContractException.

Is there a way to make it throw a specific type of exception - typically, ArgumentNullException, ArgumentOutOfRangeException, or similar, without having to go back and implement the property manually using a backing field and Requires<> ?


回答1:


No, there isn't.

But as long as your property setter is private, you don't have to worry about that. Any ArgumentException that would be thrown from your setter indicates a bug in the code calling that setter, and should be fixed there. The only code that can call your setter is your own.

If your property setter is protected or public, then you do need to specify which ArgumentException gets thrown for which values.




回答2:


From the Code Contracts manual:

Object invariants are conditions that should hold true on each instance of a class whenever that object is visible to a client. They express conditions under which the object is in a "good" state.

There's a peculiar couple of sentences in the manual at the top of page 10:

Invariants are conditionally defined on the full-contract symbol [CONTRACT_FULL]. During runtime checking, invariants are checked at the end of each public method. If an invariant mentions a public method in the same class, then the invariant check that would normally happen at the end of that public method is disabled and checked only at the end of the outermost method call to that class. This also happens if the class is re-entered because of a call to a method on another class.

— text in brackets is mine; this is the compile time symbol the manual is referencing which is defined.

Since properties are really just syntactic sugar for T get_MyPropertyName() and void set_MyPropertyName(T), these sentences would seem to apply to properties, too. Looking in the manual, when they show an example of defining object invariants, thy show using private fields in the invariant contract conditions.

Also, invariants don't really communicate to consumers of your library what the pre-conditions or post-conditions for any particular property or method are. Invariants, again, only state to consumers of the library what conditions "hold true on each instance of a class whenever that object is visible to a client." That's all they do. In order to state that if an invalid value will result in throwing an exception, you must specify a pre-condition, which is demonstrated below.

Therefore, it would appear that in order to best achieve what you're looking for, it's as hvd says: it's best to have a private backing field and place the invariant on the backing field. Then you would also provide the contracts on the property getter/setter so that consumers of your library know what the pre-conditions and guaranteed post-conditions (if any) are.

int _myPropertyField = 0;

[ContractInvariantMethod]
private void ObjectInvariants()
{
    Contract.Invariant(_myPropertyField >= 0);
}

public int MyProperty
{
    get
    {
        Contract.Ensures(Contract.Result<int>() >= 0);
        return _myPropertyField;
    }
    set
    {
        Contract.Requires<ArgumentOutOfRangeException>(value >= 0);
        _myPropertyField = value;
     }
}

Now, there is another way to throw a specific exception using "legacy" code contracts (e.g. if-then-throw contracts). You would use this method if you're trying to retrofit contracts into an existing codebase that was originally written without contracts. Here's how you can do this without using Contract.Requires<TException>(bool cond):

  1. Basically, in Section 5: Usage Guidelines of the manual, you'll be referencing Usage Scenario 3, legacy contract checking (see page 20). This means you need to set the following options in the Code Contracts project properties dialog:
    • Ensure that the Assembly Mode is set to Custom Parameter Validation.
    • Use "if-then-throw" guard blocks and perform manual inheritance.private
    • Ensure Contract.EndContractBlock() follows these guard blocks.
    • Check Perform Runtime Contract Checking and select the level of checking you want, but only on Debug builds—not on Release builds.
    • Feel free to use Contract.Requires(bool cond) (non-generic form) on private API methods (e.g. methods not directly callable by a library consumer).

Then, you could write the following code:

private int _myPropertyField = 0;

[ContractInvariantMethod]
private void ObjectInvariants()
{
    Contract.Invariant(_myPropertyField >= 0);
}

public int MyProperty
{
    get
    {
        Contract.Ensures(Contract.Result<int>() >= 0);
        return _myPropertyField;
    }
    set
    {
        if (value < 0)
        {
            throw new ArgumentOutOfRangeException("value");
        }
        Contract.EndContractBlock();
        _myPropertyField = value;
    }
}

Now, you specifically stated that you didn't want to have to go back and create private backing fields for all of your properties. Unfortunately, if this is a public property than can be mutated, then there really is no way to good avoid this. One possible way to avoid this, though, is to make your setter private:

public int MyProperty { get; private set; }

[ContractInvariantMethod]
private void ObjectInvariants()
{
    Contract.Invariant(MyProperty >= 0);
}

public SetMyProperty(int value)
{
    // Using Code Contracts with Release and Debug contract checking semantics:
    Contract.Requires<ArgumentOutOfRangeException>(value >= 0);
    // Or, using Code Contracts with Debug-only contract checking semantics:
    Contract.Requires(value >= 0);
    // Using Legacy contracts for release contract checking without throwing
    // a ContractException, but still throwing a ContractException for
    // debug builds
    if (value < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(value));
    }
    Contract.EndContractBlock();

    MyProperty = value;
}

However, I must admit, I'm not really sure what you're gaining at this point by implementing invariants in this manner. You might as well just bite the bullet and use one of the first two examples I demonstrated above.

Addendum, 2016-02-22

The OP notes in their comment that Section 2.3.1 of the manual mentions that defining object invariants on auto-properties results in the invariant essentially becoming a precondition on the setter and a postcondition on the getter. That's correct. However, the preconditions that are created use the non-generic Contract.Requires(bool condition) form. Why? So that when invariants are used by those who don't want runtime contract checking turned on for their Release builds, they can still use invariants. Therefore, even if you use invariants on properties, if you want a specific exception thrown on contract violations, you must use full properties with backing fields and the generic form of Requires, which also implies that you want to perform runtime contract checking on all builds, including Release builds.



来源:https://stackoverflow.com/questions/27597859/throw-specific-exception-for-a-violation-of-a-contract-on-an-automatic-property

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!